目指せプログラマー！

目次

アルゴリズム入門 | 2002/10/19更新：お勧め本追加
C言語入門 | 2002/10/19更新：お勧め本追加
C++言語入門 | 2002/07/27訂正：3-9修正
x86系(8086系)アセンブラ入門 | 2002/10/19更新：リンク先間違え訂正
アセンブラ ～MS-DOSの世界～ | 2001/01/20更新：エスケープシーケンスについて追加
Java入門 | 2002/11/07作成中：13章まで完成
Vector:作者ホームページ一覧


はじめに

  アルゴリズムとは直訳すると「算法」という意味です。砕いて言うと、何か問題を解決するときの考え方です。プログラムの世界では、どのような流れでプログラムを作るかという基本設計です。 フローチャート（流れ図）という言葉を聞いたことがある人がいるかと思いますが、 フローチャートとは、アルゴリズムをわかりやすく図示したもののことを言います。

  アルゴリズムの意味についての詳しい説明は後で出てきますので、 ここではこのくらいにします。

  さて、アルゴリズムはプログラムを作る上での基本設計ですので、プログラムを作らなくても 勉強できます。しかし、実際にプログラムにしてみた方が、「できたんだ!」と 実感できるでしょう。そのためにC言語も少し出てきます。ほんの少しですので、 心配しないで下さい。

← 前ページへ　:　トップへ　:　次ページへ →


1章:アルゴリズム

1-1.アルゴリズムって何？

    前にも書いたように、アルゴリズムとは、「何か問題を解決するときの考え方」です。 これをわかりやすく図示したものが、フローチャートです。

    日常生活の一場面のアルゴリズムを考えてみましょう。今あなたが道を歩いていて、 信号にさしかかりました。信号を見ると赤だったので、そのまま信号を見ながら待ちました。 しばらくして青になったので、わたりました。これは立派(?)なアルゴリズムです。 これをフローチャートにするとこのようになります。

  では、フローチャートは何のために作るのでしょう？
  • 図で示されているので、文章だけよりもわかりやすく、プログラムにしやすい
  • バージョンアップなど改良する際に効率よくできる
  • １つのプログラムを複数人で作るときに、手分けしやすい
  • 同じような処理は、再利用できる
といったメリットがあります。


1章:アルゴリズム

1-2.当ページの範囲

    このホームページ（アルゴリズム入門）では、「問題」→「アルゴリズム」→ 「フローチャート」→「C言語によるプログラム」までのうち、アンダーラインの部分を 「アルゴリズム入門」として載せ、それを目に見える結果に出力するためにプログラムを C言語で行う。C言語プログラムを学ぶのが目的ではないので、C言語についてのコメントには 「おまじないだよ」というコメントを多用しているが、気が向いたら、C言語のページも 作る予定なので、そちらを参照して下さい。

  アルゴリズム　：　与えられた問題の解き方
      ↓
  フローチャート：　アルゴリズムを図示したもの
      ↓
  プログラム　　：　プログラム言語による表現

問題）
           台形の上底、下底、高さをキーボードから入力し、面積を求めよ。
ただし、どれかが0だったら終了する。

#include
void main(){
    int joutei,katei,takasa,t;

    scanf("%d",&joutei);
    scanf("%d",&katei);
    scanf("%d",&takasa);

    t=(joutei+katei)*takasa/2;
    if(t != 0)
        printf("面積は%dです。\n",t);
    }

}
scanf("%d",&joutei); | キーボードから入力し、数字として変数jouteiに代入する。
t=(joutei+katei):takasa/2 | 面積の計算
if(t != 0) | もしtが0でなければ、次の1行を実行する
printf("・・・") | 結果を表示

  上記のうち、青い部分を答えてもらうようにし、黒い部分（直接フローチャートとは 関係ない部分）は、今のところ考える（覚える）必要はない。


1章:アルゴリズム

1-3.よいアルゴリズム

    与えられた問題を解く方法は１つとは限らない。いくつかある解法の中から、 １番良いアルゴリズムを見つけなければならない。

    では、良いアルゴリズムとはなんだろうか？

  • 誰が見てもわかりやすい
  • どんな場合でも正しく処理する（ユーザーがどんな使い方をしても）
  • プログラムの効率がよい
  • 同じような処理は、違うプログラムを作っているときでも再利用できる。

    特殊な場合以外は、処理が速く、メモリーが少なくなるような組み方は最近ではしない。 如何に、バグを出さないか、手直ししやすいか、といったことを重視する。すなわち、 １番の「誰が見てもわかりやすい」は何よりも優先する。

    長いプログラムをつくると、見直したときに自分でも何やっている部分かわからなくなる ことが多々ある。そういった意味でも重要である。


1章:アルゴリズム

1-4.フローチャートの書き方(記号)

    フローチャートの記号は日本ではJIS規格(日本工業規格)で決められている。したがって好き勝手に記号を作ったりしてはいけない。

 端子 フローチャートの始まり及び終わりを表す
処理 計算、代入などの処理を表す
判断 条件によって分岐する
ループ 繰り返しの始まりと終わり
入力 キーボードからの入力
表示 ディスプレイへの表示
サブルーチン サブルーチン(モジュール)
書類 印刷する
入出力 ファイルへの入出力
結合子 ページ内の結合
結合子 ページ外の結合

  1-1で紹介したフローチャートを見直します。

  • (1)プログラムの最初と、最後を明記する。
  • (2)処理の流れは原則として、上から下へ、左から右へ。それに逆行する際には、矢印にする。
  • (3)線が交差しないようにする。


1章:アルゴリズム

1-5.フローチャートの書き方(構造)

    フローチャートは基本的に３つの構造を組み合わせて作る。

  • 順次構造
  • 分岐構造
  • 反復構造

    原則的には、これらの組み合わせでどんなアルゴリズムも表記できる。


1章:アルゴリズム

1-6.順次構造

    順次構造とはその名の通り、処理が上から下へ単純に並んでいる構造を言う。最も単純な構造なので、手直しなども簡単である。順次構造の例を右に示した。


1章:アルゴリズム

1-7.分岐構造

    分岐構造とは、ある条件によって処理が別れる構造である。条件は「yes」か「no」で 表される物が多く単純であるが、いくつかの条件を同時に満たしているとか、 複数の条件のうち一つ以上を満たしているとか、ある値が１だったら○○○、 ２だったら△△△、３だったら◇◇◇、それ以外だったら×××、というような分岐もある。
    ここで重要なことは、処理が別れても、後に合流することである。 これ以外にも決まりがあるが、これについては３章で説明する。


1章:アルゴリズム

1-8.反復構造

    反復構造とは、ある条件を満たしているうちは(満たさないうちは)、 処理を実行するという構造である。この条件をきちんと設定しておかないと、 いつまでたっても反復処理から抜け出せず、無限ループとなる。

    また、条件をどこで設定するかによって、２種類に分けられる。 すなわち右上図のように、前判定型と右下図のように後判定型である。 ここで「前」とか「後」というのは、処理に対してという意味で、 前判定型は条件によっては処理を一度も行わない。これに対して後判定型は、 どんな条件でも最低１回は処理を行う。

    反復構造は４章で詳しく述べる。


2章:順次構造

2-1.代入

    マシン語以外の変数はすべてメモリー上に記憶されている。この変数へ数字や文字、文字列を 格納することを代入という。マシン語以外では変数はメモリーに格納されていることを 意識しなくてもなんとかなる。詳しいことは、言語のところで説明するが、 フローチャートではどのように表記するのかをここで述べる。

    代入は２種類に大別できる。１つは変数から変数への代入、もう１つは 変数へ定数の代入である。

変数から変数への代入

    フローチャートでは次のように代入を記述する。(A1,A2は変数)

    A1に100が入っている場合は下記のように、A1、A2とも100となる。

    プログラムでは、A2にA1を代入するときは「A2=A1」と記入する言語が多い。そこで右辺と 左辺を逆にして、

と記入することもある。

    前者の書き方はJIS規格の書き方であり、 後者の書き方はプログラム言語風であるといえる。

変数へ定数の代入

    同様に、定数を代入するときは下記のようになる。
    ただし、定数が数字の場合は普通に数字を書けばよいが、文字(１文字)の時は、「'」で囲み、 文字列の時は「"」で囲む。

C言語での表現

    C言語では、変数を使うのに「こんな変数を使うよ」と宣言しなければいけません。 ここではC言語の勉強ではないので、宣言の説明は省略して、代入のみ説明します。

a2 = a1; | 　　a1をa2に代入する
a2 = 100; | 　　a2に100を代入する
a2 = 'a'; 　　a2に'a'という文字の
    文字コード(1バイト)を代入する

アセンブラでの表現

    アセンブラでも変数を使う前に「こんな変数を使うよ」と宣言しなければなりません。 やはりここでは代入についてのみ説明します。

    なお、わからなくてもアセンブラのページで紹介するつもりなので、心配いりません。

mov　ax,r1
mov　r2,ax 		　　a1をa2に代入する
mov　a2,100 　　a2に100を代入する
mov　a2,'a' 　　a2に'a'という文字の
    文字コード(1バイト)を代入する


2章:順次構造

2-2.算術計算

    算術演算とは、足し算、引き算、かけ算、わり算といった四則演算をはじめとして、 平方根、べき乗などの計算のことです。ここでは、最初にあげた四則演算しか使わないので、 ほかの演算については省略します。C言語ではマイナスの値も普通の整数のように 扱えますが、アセンブラではちょっと面倒なので、アセンブラのページに譲ります。 また、小数点は本ページでは扱いません(わり算は小数点以下が切り捨てになります)。

    パソコンのキーボードには「÷」「×」といったキーがないので、アセンブラを除く ほとんどのプログラム言語では別の文字を使います。フローチャートでも下記のように ほとんどのプログラム言語で使われている文字をそのまま使います。
+　加算
-　減算
*　乗算
/　除算

フローチャートでは計算結果を変数に代入する場合、下図のようになります。

  また、算数の授業で習ったように剰余計算は、加減算よりも優先します。 加減算を優先させたい場合にはカッコが使えます。
  1. 符号(+1とか-5などの、+,-)
  2. カッコで括られた計算
  3. 乗算、除算
  4. 加算、減算

C言語での表現

c = a + b;
c = (a + 3) * b;

アセンブラでの表現

mov　ax,a
add　ax,b
mov　c,ax


2章:順次構造

2-3.入力

    文字通り、キーボードからの入力です。キーボードから入力にはその内容によって 以下のような種類がある。
  • 数字の入力
  • 1文字入力
  • 文字列の入力

    フローチャート上では厳密に区別する必要はないが、プログラムにした場合には 注意が必要となる。

C言語での表記

scanf("%d",&a); | 　　数字を入力して変数aに代入
scanf("%d%d",&a,&b); | 　　数字を2つ入力して変数a,bに代入
scanf("%s",str); | 　　文字列を入力して変数strに代入
c=getchar(); | 　　1文字入力し、変数cに代入

アセンブラでの表記

    アセンブラでは、数の入力という概念がありません。1文字もしくは文字列を入力し、 これが数字であるかどうかを判断して、文字コードもしくは文字コードの集まりを 数字に変換しなければなりません。これはアセンブラのページで紹介します。
    ここでは1文字もしくは文字列の入力例を紹介します。
mov　ah,1
int　21h
mov　chara,al 		　　1文字入力し、変数charaに代入します

mov　ah,0ah
mov　dx,offset str
int　21h 		　　文字列を入力し、変数strに代入します
    ただし、あらかじめ変数strに細工する
    必要があります


2章:順次構造

2-4.出力

    文字通り、ディスプレイへの出力です。出力はその内容によって 以下のような種類があります。
  • 数字の出力
  • 1文字出力
  • 文字列の出力

    フローチャート上では厳密に区別する必要はないが、プログラムにした場合には 注意が必要となる。

C言語での表記

printf("%d",&a); | 　　変数aを数字として出力
printf("%d %d",&a,&b); | 　　変数a,bを数字として出力
prinntf("%s",str); | 　　変数str(文字列)を出力
putchar(); | 　　変数cに入っている1文字を出力

アセンブラでの表記

  アセンブラでは、数の出力という概念がありません。数として出力する場合は 数を各文字コードに変換して1文字ずつもしくは文字列として出力しなくては いけません。これはアセンブラのページで紹介します。
  ここでは1文字もしくは文字列の出力例を紹介します。
mov　ah,2
mov　dl,a
int　21h 		　　変数aに入っている1文字を出力

mov　ah,9
mov　dx,offset str
int　21h 		　　strに入っている文字列を出力


2章:順次構造

2-5.練習問題(その1)

  問題：
変数x,yに1文字ずつ入力し、その変数を交換後、結果を出力するフローチャートを書け。

  ヒント：
xをyに、yをxに代入するだけでは、yをxに代入する前に、yの値が壊れてしまうことに注意


2章:順次構造

2-5.練習問題(その1)

  問題：
変数x,yに1文字ずつ入力し、その変数を交換後、結果を出力するフローチャートを書け。

  解答：
フローチャートがあっていたら、C言語でプログラムを書いてみよう!
void main(){
    char x,y,w;

                         ;

       ;
       ;
       ;

                           ;
}

1行目の「void main(){」はおまじないです。
2行目の「char x,y,w」は変数を使うという宣言です。
では、アンダーラインのところを埋めましょう。


2章:順次構造

2-5.練習問題(その1)

  問題：
変数x,yに1文字ずつ入力し、その変数を交換後、結果を出力するフローチャートを書け。

  解答：
void main(){
    char x,y,w;

    scanf("%c %c", &x, &y);

    w=x;
    x=y;
    y=w;

    printf("x=%c y=%c\n", x, y);
}

4行目はx,yに1文字ずつ代入しています。
6～8行目で交換しています。
10行目で、表示しています。
「\n」は改行するという意味です。


2章:順次構造

2-5.練習問題(その1)

  問題：
変数x,yに1文字ずつ入力し、その変数を交換後、結果を出力するフローチャートを書け。

  解答：
    .model small
    .data
X	byte	?
Y	byte	?
W	byte	?
    .code
    .startup

    mov	ah,1
    int	21h
    mov	X,al	;1文字入力し、Xに代入

    mov	ah,1
    int	21h
    mov	Y,al	;2文字めを入力し、Yに代入

    mov	al,X
    mov	W,al	;X→W

    mov	al,Y
    mov	X,al	;Y→X

    mov	al,W
    mov	Y,al	;W→Y

    mov	ah,2
    mov	dl,X
    int	21h	;X出力

    mov	ah,2
    mov	dl,Y	;Y出力
    int	21h

    .exit	0
    end

意味のない、無駄の多いプログラムだなぁ。


2章:順次構造

2-6.練習問題(その2)

  問題：
変数x,yに数字を入力し、加算、減算、乗算、除算を行い結果を表示せよ。


2章:順次構造

2-6.練習問題(その2)

  問題：
変数x,yに数字を入力し、加算、減算、乗算、除算を行い結果を表示せよ。

void main(){
    int x,y,w1,w2,w3,w4;

                         ;

          ;
          ;
          ;
          ;

                              ;
}

1行目の「void main(){」はおまじないです。
2行目の「int x,y,w」は変数を使うという宣言です。
では、アンダーラインのところを埋めましょう。


2章:順次構造

2-6.練習問題(その2)

  問題：
変数x,yに数字を入力し、加算、減算、乗算、除算を行い結果を表示せよ。

void main(){
    int x,y,w1,w2,w3,w4;

    scanf("%d %d", &x, &y);

    w1=x+y;
    w2=x-y;
    w3=x*y;
    w4=x/y;

    printf("%d %d %d %d\n",w1,w2,w3,w4);
}

できましたか？


3章:分岐構造

3-1.条件分岐

    条件分岐とはその名の通り、条件によって異なる処理をします。すなわち、 ある条件を満たしていれば(満たしていなければ)Aという処理を、満たして いなければ(満たしていれば)Bという処理をする、といった感じです。

    条件分岐は、分岐の仕方で次の３つに大別できます。

単一分岐 | 　　処理を２つに分ける。もっとも単純な分岐
多重分岐 | 　　単一分岐を複数組み合わせたもの。
ケース(多方向分岐) | 　　複数の条件の中から当てはまる１つの処理を実行

    一般に条件とは２つの値の大小関係を比較することが多い。フローチャートでは、 以下の記号を用いる。

＜ 小さい（未満、以上でない）
≦ 以下(大きくない)
＝ 等しい
≧ 以上(小さくない)
＞ 大きい（超過、以下でない)
≠ 等しくない
＜＞ 等しくない

    ただし、文字の比較を行う場合、コンピュータの種類によって異なる場合がある。


3章:分岐構造

3-2.単一分岐

    分岐の条件が１つだけの場合を単一分岐という。フローチャートの書き方は以下のようになる。 また、C言語の書き方を青で、アセンブラでの書き方を 緑で示した。

変数nが0なら処理1を、
0以外なら処理2を実行する 		nがm以上なら処理1を、
未満なら処理2を実行する
    if(n==0){
        処理1;
    }
    else{
        処理2;
    }

    if(n>=m){
        処理1;
    }
    else{
        処理2;
    }

    cmp	n,0
    jz	SHORI1
      :
    処理2
      :
    jmp	SHORI_END
SHORI1:
      :
    処理1
      :
SHORI_END:

    mov	al,n
    cmp	al,m
    jae	SHORI1
      :
    処理2
      :
    jmp	SHORI_END
SHORI1:
      :
    処理1
      :
SHORI_END:

charの内容が'A'であれば処理1を実行し、
それ以外ならば何もしない
同上
こういう書き方もある

    if(ch=='A'){
        処理1;
    }
    else{
        処理2;
    }

    cmp	ch,'A'
    jz	SHORI1
      :
    処理2
      :
    jmp	SHORI_END
SHORI1:
      :
    処理1
      :
SHORI_END:

    どの例もすべて、処理をし終わった後は合流していることが重要です。 これは多重分岐などの場合も含めて、必ず合流しなくてはなりません。


3章:単一分岐

3-3.練習問題

  問題：
キーボードより数を入力し、その値が100以下なら「小さい」と 100を越えていれば「大きい」と表示させよ。


3章:単一分岐

3-3.練習問題

  問題：
キーボードより数を入力し、その値が100以下なら「小さい」と 100を越えていれば「大きい」と表示させよ。

  解答：

void main(){
    int x;

    /*ここに入力しよう*/

}


3章:単一分岐

3-3.練習問題

  問題：
キーボードより数を入力し、その値が100以下なら「小さい」と100を越えていれば 「大きい」と表示させよ。

  解答：

void main(){
    int x;

    scanf("%d",&x);
    if(x<=100)
        printf("小さい\n");
    else
        printf("大きい\n");
}


3章:分岐構造

3-4.多重分岐

    多重分岐とは、単一分岐を組み合わせたもので、単一分岐の処理の中にまた条件分岐を 含むものである。

C言語での表記
if(条件1){
    if(条件2){
        処理3;
    }
    else{
        処理2;
    }
}
else{
    処理1;
}

    条件1
    jz	JOUKEN2
      :
    処理1
      :
    jmp	short JOUKEN_END
JOUKEN2:
    条件2
    jz	SYORI3
      :
    処理2
      :
    jmp	short JOUKEN_END
SHORI3:
      :
    処理3
      :
JOUKEN_END:


3章:分岐構造

3-4.多重分岐(例題)

  例題：
変数a,b,cに適当な数が代入されている。この変数中で最小値を変数wに代入する。

  考え方：
aとbを比べる。そのうち、小さい方とcを比べる。その結果小さい方が最小値である。

  下のアンダーラインを埋めよ。
  • a≦bでかつa≦cの時　→　最小値は
  • a≦bでかつa＞cの時　→　最小値は
  • a＞bでかつb≦cの時　→　最小値は
  • a＞bでかつb＞cの時　→　最小値は


3章:分岐構造

3-4.多重分岐(例題)

  例題：
変数a,b,cに適当な数が代入されている。この変数中で最小値を変数wに代入する。

  考え方：
aとbを比べる。そのうち、小さい方とcを比べる。その結果小さい方が最小値である。

  • a≦bでかつa≦cの時　→　最小値は　a
  • a≦bでかつa＞cの時　→　最小値は　c
  • a＞bでかつb≦cの時　→　最小値は　b
  • a＞bでかつb＞cの時　→　最小値は　c

これをアルゴリズムで表すと、
if(a<=b){
    if(a<=c)
        w=a;
    else
        w=c;
    }
else{
    if(b<=c)
        w=b;
    else
        w=c;
    }
}

    mov	al,a
    cmp	al,b
    jbe	JOUKEN2

    mov	al,b
    cmp	al,c
    jbe	MIN_B

    mov	al,c
    mov	w,al
    jmp	short _END
MIN_B:
    mov	al,b
    mov	w,al
    jmp	short _END
JOUKEN2:
    mov	al,a
    cmp	al,c
    jbe	MIN_A

    mov	al,c
    mov	w,al
    jmp	short _END
MIN_A:
    mov	al,a
    mov	w,al
;	jmp	short _END
_END:

    ここで注意点があります。上のフローチャートで、cをwに代入している操作が ２カ所あります。しかしこれをまとめて、下記のようなフローチャートにする ことはできません。アセンブリ言語のようにプログラム言語によっては、 このような悪いフローチャートをプログラムとして書くことが日常茶飯事に 行われる言語もありますが、いけません。必ず、条件分岐したものは、 一番手前の分岐が１つにまとまってからでないと、他の処理とまとまることは 許せないのです。

    mov	al,a
    cmp	al,b
    jbe	JOUKEN2

    mov	al,b
    cmp	al,c
    jbe	MIN_B
MIN_C:
    mov	al,c
    mov	w,al
    jmp	short _END
JOUKEN2:
    mov	al,a
    cmp	al,c
    ja	MIN_C

    mov	al,a
    mov	w,al
;	jmp	short _END
_END:

↑これは悪い例

← 前ページへ　:　トップへ　:　次ページへ →


3章:分岐構造

3-4.多重分岐

    前のページでは、多重分岐を用いて最小値を求めた。しかしこのような方法では、 ３つの変数から最小値を求めるだけで、条件分岐を３つも用いており、変数が増えた場合 とんでもないことになってしまう。そこで「書き換え法(仮称)」という方法がある。

  例題：
変数a,b,cに適当な数が代入されている。この変数中で最小値を変数wに代入する。

  考え方：
これはとりあえず１つめの変数を仮の最小値とする。 次の変数を仮の最小値と比較して小さければ、その値を仮の最小値にする。 これを変数の数だけ繰り返す。

void main(){
    int a,b,c,w;
    a=100;
    b=200;
    c=300;

    w=a;

    if(b<w)
        w=b;

    if(c<w)
        w=c;

    printf("最小値は%dです\n",w);
}

    .model	small
    .data
A	byte	100
B	byte	200
C	byte	300
W	byte	?

    .code
    .startup
    mov	al,A
    mov	W,al

    mov	al,b
    cmp	al,W
    jnb	CHECK_C

    mov	W,al

CHECK_C:
    mov	al,c
    cmp	al,W
    jnb	CHECK_END

    mov	W,al

CHECK_END:
;	表示部は省略

    .exit	0
    end

  変数が多くなればこの方法でも、どんどんプログラムが長くなります。 もちろん実際のプログラムでは、どんどん長く書くわけではありません。 チェックする部分は同じような処理をさせているわけですので、後で述べる 反復構造を利用するのです。しかし、考え方は この「書き換え法(仮称)」と全く同じです。


3章:分岐構造

3-5.複合条件

  例題：
変数a,b,cに適当な数が代入されている。この変数中で最小値を変数wに代入する。
  考え方：
aとbを比べる。そのうち、小さい方とcを比べる。その結果小さい方が最小値である。

  • a≦bでかつa≦cの時　→　最小値は　a
  • a≦bでかつa＞cの時　→　最小値は　c
  • a＞bでかつb≦cの時　→　最小値は　b
  • a＞bでかつb＞cの時　→　最小値は　c

  これは前のページでやった例題を多重分岐を使う場合の考え方です。しかし 複合条件を用いるともう少しわかりやすくなります。複合条件とは、ある条件 と別の条件を両方満たしている場合( AND複合条件)や、どちらか一方を満たしている場合 (OR複合条件)など、 複数の条件を一つの条件として分岐することを言います。 アセンブリ言語などプログラム言語によっては、このような複合条件を扱えない 言語もあり、この場合は多重分岐になります。

  このような考えでフローチャートを書くと次のようになります。

if(a<=b && a<=c)
    w=a;
if(a<=b && a>c)
    w=c;
if(a>b && b<=c)
    w=b;
if(a>b && b>c)
    w=c;

  フローチャートでは「AND」とか「OR」と表記するのに対して、C言語では、 ANDを「&&」、 ORを「||」で表記します。


3章:分岐構造

3-6.ケース(多方向分岐)

    ３つ以上の異なった処理の中から、条件にあった処理を行うことを多方向分岐と 言い、次のような書き方をする。なお、アセンブリ言語などこのようなアルゴリズムを サポートしていない言語もある。

例）　charaの内容が'A'だったらwに65を、'B'ならば66をそれ以外なら0を代入する。

switch(chara){
    case 'A' : w=65; break;
    case 'B' : w=66; break;
    default  : w=0 ; break;
}

    なお、C言語表記中の「break;」を忘れ、下のようなプログラムにすると、 左のフローチャートになる。しかし、このようなフローチャートは悪い見本であり、 正式なフローチャートではない。(正しく動かないという意味ではなく、 このようなアルゴリズムがきれいなアルゴリズムではないという意味。)

switch(chara){
    case 'A' : w=65;
    case 'B' : w=66;
    default  : w=0 ;
}


3章:分岐構造

3-7.練習問題2

  問題：
１文字入力し、大文字('A'～'Z')ならば"大文字と、 小文字('a'～'z')ならば"小文字"と表示せよ。


3章:分岐構造

3-7.練習問題2

  問題：
１文字入力し、大文字('A'～'Z')ならば"大文字と、小文字('a'～'z')ならば "小文字"と表示せよ。

void main(){
    char c;

    /* ここに入力してね/*

}


3章:分岐構造

3-7.練習問題2

  問題：
１文字入力し、大文字('A'～'Z')ならば"大文字と、 小文字('a'～'z')ならば"小文字"と表示せよ。

void main(){
    char c;

    scanf("%c", &c);
    if('A' <= c and c <= 'Z')
        printf("大文字\n";
    if('a' <= c and c <= 'z')
        printf("小文字\n";
}


3章:分岐構造

3-7.練習問題2

  問題：
１文字入力し、大文字('A'～'Z')ならば"大文字と、 小文字('a'～'z')ならば"小文字"と表示せよ。

    .model	small
    .data
C	byte	?
BIG_CH	byte	"大文字です",0dh,0ah,24h
SML_CH	byte	"小文字です",0dh,0ah,24h
    .code
    .start up

    mov	ah,1
    int	21h		;al <= 1文字入力
    mov	C,al

    mov	al,C
    cmp	al,'A'		;1つめの分岐
    jb	JOOUKEN3

    cmp	al,'Z'		;2つめの分岐
    ja	JOUKEN3

    mov	ah,9
    mov	dx,offset BIG_SML
    int	21h		;"大文字"表示
JOUKEN3:
    mov	al,C
    cmp	al,'a'		;3つめの分岐
    jb	JOUKEN_END

    cmp	al,'z'		;4つめの分岐
    ja	JOUKEN_END

    mov	ah,9
    mov	dx,offset SML_CH
    int	21h		;"小文字"表示
JOUKEN_END:


3章:分岐構造

3-8.練習問題3

  問題：
数字を入力し、1なら「島袋寛子」、2なら「今井絵里子」、3なら「上原多香子」、 4なら「新垣仁絵」、それ以外なら「モーニング娘」と表示し、その後「が好き」と 表示する


3章:分岐構造

3-8.練習問題3

  問題：
数字を入力し、1なら「島袋寛子」、2なら「今井絵里子」、3なら「上原多香子」、 4なら「新垣仁絵」、それ以外なら「モーニング娘」と表示し、その後「が好き」と 表示する
void main(){
    char c;

    /* ここに入力してね/*

}


3章:分岐構造

3-8.練習問題3

  問題：
数字を入力し、1なら「島袋寛子」、2なら「今井絵里子」、3なら「上原多香子」、 4なら「新垣仁絵」、それ以外なら「モーニング娘」と表示し、その後「が好き」と 表示する
void main(){
    char c;

    scanf("%c", c);

    switch(c){
        case '1': printf("島袋寛子");break;
        case '2': printf("上原多香子");break;
        case '3': printf("今井絵里子");break;
        case '4': printf("新垣仁絵");break;
        default : printf("モーニング娘");break;
    }
    printf("が好き\n");
}

]
    .model	small
    .data
HIROKO	byte	"島袋寛子$"
TAKAKO	byte	"上原多香子$"
ERIKO	byte	"今井絵里子$"
HITOE	byte	"新垣仁絵$"
MUSUME	byte	"モーニング娘$"
SUKI	byte	"が好き",0dh,0ah,24h
C	byte	?

    .code
    .startup

    mov	ah,1
    int	21h
    mov	C,al		;1文字入力

CASE1:
    mov	al,C
    cmp	al,'1'
    jnz	CASE2		;case '1'

    mov	ah,9
    mov	dx,offset HIROKO
    int	21h		;"島袋寛子"表示

    jmp	short CASE_END	;break;

CASE2:
    mov	al,C
    cmp	al,2'
    jnz	CASE3		;case '2'

    mov	ah,9
    mov	dx,offset TAKAKO
    int	21h		;"上原多香子"表示

    jmp	short CASE_END	;break;

CASE3:
    mov	al,C
    cmp	al,3'
    jnz	CASE4		;case '3'

    mov	ah,9
    mov	dx,offset ERIKO
    int	21h		;"今井絵里子"表示

    jmp	short CASE_END	;break;

CASE4:
    mov	al,C
    cmp	al,2'
    jnz	CASE5		;case '4'

    mov	ah,9
    mov	dx,offset HITOE
    int	21h		;"新垣仁絵"表示

    jmp	short CASE_END	;break;

CASE5:
    mov	ah,9
    mov	dx,offset MUSUME
    int	21h		;"モーニング娘"表示

CASE_END:
    mov	ah,9
    mov	dx,offset SUKI
    int	21h		;"が好き"表示

    .exit	0
    end
}


4章:分岐構造

4章:反復構造

4-1.反復構造の種類

    反復構造はある処理を1) 条件を満たす間もしくは2) 満たすまで繰り返す構造のことである。

条件を満たす間繰り返す 　　条件を満たすまで繰り返す

C言語的
    COBOL的

  反復構造は次の２種類に大別される。
  • 前判定型・・・処理の前に反復条件を設置する
  • 後判定型・・・処理の実行後に反復条件を設置する
  これをフローチャート、C言語、アセンブリ言語で表すと以下のようになる。
while(条件){
      :
    処理
      :
}

    WHILE_START:
        条件
        jnz	WHILE_END
          :
        処理
          :
        jmp	short WHILE_START
    WHILE_END:

do{
      :
    処理
      :
}while(条件);

    WHILE_START:
          :
          処理
          :
        条件
        jz	WHILE_START
    WHILE_END:

    前判定型では、条件によっては１度も処理を実行しない場合がある。これに対して 後判定型では、どのような条件でも必ず１度は処理が実行される。


4章:分岐構造

4-1.反復構造の種類2

    フローチャートでは処理の直前、もしくは直後にしか書けない。したがって、 左下のようなフローチャートは悪い例である。(プログラムでは書けるけれど・・・) もし書きたい場合は、面倒ではあるが右下のように書く

悪い例

良い例

    また反復構造を示す記号として、専用の記号も用意されている。この場合、条件には 反復構造を抜ける条件を書くことが決められている。

    C言語では条件を満たしているあいだ繰り返す。フローチャートでは逆に 条件を満たしたら抜けるので、フローチャートをプログラムにする場合 注意が必要となる。

    また、ループ記号を使わない場合と同様、ループの中から条件分岐等を 用いて勝手にループから抜け出してもいけません。ただし、次のように、 条件分岐を入れる場合は大丈夫ですし、中にループ記号を繰り返し入れる (ネストする、入れ子にする)ことも許されます。


4章:分岐構造

4-2.前判定型

    では、１から100までの合計を求めるフローチャートを考える。高校ではこのような 公式を習いましたが、その公式を使ったのでは一発で求めることができて、反復処理の 練習にはならないので、すなおに「1+2+3+4+・・・+99+100」を計算します。

    すなわちn=1にしておいて、nの値を1ずつ加算していきます。それと同時にtにnを加えて 行けばよいことになります。これをフローチャートにすると下のようになります。ループ記号を使ったものは 右のようになりますが、同じフローチャートです。

void main(){
    int n, t;

    n=1;
    t=0;

    while( n <= 100 ){
        t = t + n;
        n++;
    }
    printf("合計は%dです\n",t);
}

    ここでは2ページ前のC言語表記の例にしたがって、「while」を用いましたが、 C言語には便利な「for」文が用意されています。ここではC言語の勉強ではないので 詳しい説明は省きますが、使用例を紹介します。

void main(){
    int n, t;
    t = 0;

    for(n = 1 ; n <= 100 ; n++)
        t = t + n;

    printf("合計は%dです\n",t);
}

for(式1; 式2; 式3){
      :
    処理;
      :
}
  とは、ようするにこういうことです。


4章:分岐構造

4-3.後判定型

    では、前ページの1から100までの合計を後判定で行うとどうなるだろうか？

void main(){
    int n, t;

    n=0;
    t=0;

    do{
        n++;
        t=t+n;
    }while(n<100);

    printf("1から100までの合計は%dです\n",t);
}


4章:分岐構造

4-4.練習問題1

  問題：
1から100までの偶数の合計を求めよ。

  ヒント：
偶数を求める方法はいくつかある。解答では4種類紹介するので、余裕のある人は 考えてみてください。


4章:分岐構造

4-4.練習問題1

  問題：
1から100までの偶数の合計を求めよ。

  ヒント：
偶数を求める方法はいくつかある。解答では4種類紹介するので、余裕のある人は 考えてみてください。
  1. 0から100まで、2ずつ変数nを増やしていく方法
  2. 100から0まで、2ずつ変数nを減らしていく方法
  3. 0から50まで、変数nを1づつ増やしていき、合計を求めるときにnの2倍を加えていく方法
  4. 0から100まで、変数nを1づつ増やしていき、偶数のみを加算していく方法
          以下、これらの方法についてフローチャートを紹介します。

  1. 0から100まで、2ずつ変数nを増やしていく方法

  2. 100から0まで、2ずつ変数nを減らしていく方法

  3. 0から50まで、変数nを1づつ増やしていき、合計を求めるときにnの2倍を加えていく方法

  4. 0から100まで、変数nを1づつ増やしていき、偶数のみを加算していく方法

※論理積(and演算)は [image] で表す。
  論理和(or演算)は [image] で表す。


4章:分岐構造

4-4.練習問題1

  問題：
1から100までの偶数の合計を求めよ。

  ヒント：
偶数を求める方法はいくつかある。解答では4種類紹介するので、余裕のある人は 考えてみてください。
  1. 0から100まで、2ずつ変数nを増やしていく方法
  2. 100から0まで、2ずつ変数nを減らしていく方法
  3. 0から50まで、変数nを1づつ増やしていき、合計を求めるときにnの2倍を加えていく方法
  4. 0から100まで、変数nを1づつ増やしていき、偶数のみを加算していく方法
          以下、これらの方法についてフローチャートを紹介します。

  1. 0から100まで、2ずつ変数nを増やしていく方法

    void main(){
    int n,t;

    n=0;
    t=0;

    while(n<100){
        n=n+2;
        t=t+n;
    }

    printf("1から100までの偶数の合計は %dです\n",t);
}

  2. 100から0まで、2ずつ変数nを減らしていく方法

    void main(){
    int n,t;

    n=100;
    t=0;

    while(n>0){
        t=t+n;
        n=n-2;
    }

    printf("1から100までの偶数の合計は %dです\n",t);
}

  3. 0から50まで、変数nを1づつ増やしていき、 合計を求めるときにnの2倍を加えていく方法

    void main(){
    int n,t;

    n=0;
    t=0;

    while(n*2<100){
        n=n+1;
        t=t+2*n;
    }

    printf("1から100までの偶数の合計は %dです\n",t);
}

  4. 0から100まで、変数nを1づつ増やしていき、 偶数のみを加算していく方法

void main(){
    int n,t;

    n=0;
    t=0;

    while(n<=0){
        if(n & 1=0
            t=t+n;
        n=n+115:41 99/08/01;
    }

    printf("1から100までの偶数の合計は %dです\n",t);
}


4章:分岐構造

4-5.練習問題2

  問題：
二つの数字m,nをキーボードから入力し、mからnまでの合計を求める。

  ヒント：
つねに、m<nとは限らない。人が行う動作には間違えもある。プログラムは、 どんな条件でも暴走したりしてはいけないのである。つまりプログラマーは どんな場合があるかを常に気にとめる必要があり、今回のように、ただ単に、 mからnまでを考えるのではなく、nからmまでという場合も考慮する必要がある。


4章:分岐構造

4-5.練習問題2

  問題：
二つの数字m,nをキーボードから入力し、mからnまでの合計を求める。

  ヒント：
つねに、m<nとは限らない。人が行う動作には間違えもある。プログラムは、 どんな条件でも暴走したりしてはいけないのである。つまりプログラマーは どんな場合があるかを常に気にとめる必要があり、今回のように、ただ単に、 mからnまでを考えるのではなく、nからmまでという場合も考慮する必要がある。

void main()
{
    int   m,  n, sum, i ;
    printf("ｍからｎまでの合計を求めます\n\n");
    printf("m >> ");
    scanf(" %d", &m);
    printf("n >> ");
    scanf(" %d", &n);

    sum=0;

    if(m>n){
        w=m;
        m=n;
        n=w;
    }
    sum=0;
    i=m;

    while(i<=n){
        sum=sum+i;
        i=i+1;
    }

    printf("%d から %d の合計 = %d\n",m,n,sum);
}


4章:分岐構造

4-6.練習問題3(ユークリッドの互除法)

  問題：
二つの数字m,nをキーボードから入力し、最大公約数を求める。

  ヒント：
  1. ２つの数字のうち、大きい方をa、小さい方をbに代入する。
  2. a÷bを計算し、あまりをrに代入する。
  3. r＝0ならば、bが最大公約数である。
  4. r≠0ならば、bをaに代入し、rをbに代入する。そして2から繰り返す。

  例）m=252、n=98の場合
処理回数 | a | b | r
1 | 252 | 98 | 56
2 | 98 | 56 | 42
3 | 56 | 42 | 14
4 | 42 | 14 | 0
  したがって、最大公約数は14である。


4章:分岐構造

4-6.練習問題3(ユークリッドの互除法)

  問題：
二つの数字m,nをキーボードから入力し、最大公約数を求める。

  ヒント：
  1. ２つの数字のうち、大きい方をa、小さい方をbに代入する。
  2. a÷bを計算し、あまりをrに代入する。
  3. r＝0ならば、bが最大公約数である。
  4. r≠0ならば、bをaに代入し、rをbに代入する。そして2から繰り返す。

  例）m=252、n=98の場合
処理回数 | a | b | r
1 | 252 | 98 | 56
2 | 98 | 56 | 42
3 | 56 | 42 | 14
4 | 42 | 14 | 0
  したがって、最大公約数は14である。

void main(){
    int a,b,m,n,r;

    printf("２つの自然数を入力してください\n  m >> ");
    scanf("%d",&m");
    printf("  n >> ");
    scanf("%d",&n");

    if(m>n){
        a=m;
        b=n;
    }
    else{
        a=n;
        b=m;
    }

    r=a % b;

    while(r!=0){
        a=b;
        b=r;
        r=a % b;
    }

    printf("\n%dと%dの最大公約数は%dです。\n",m,n,b);
}


4章:分岐構造

4-7.多重反復処理(ネスト)

    前にも書いたように、反復処理の中に、反復処理を入れることもできます。このようなことを ネストする、入れ子にするなどと呼びます。

  やはり多重反復処理でも、ループの中から抜け出してはいけません。


4章:分岐構造

4-8.練習問題

  問題：
50円切手と、80円切手を買いに、 東急ストアに行きました。 所持金はキーボードから入力された額です。でもお釣りをもらうのは面倒なので、 所持金ぴったりの額で買いたいです。何枚ずつ買えば良いでしょう？

  ヒント：
これは小学校の時にやった「鶴亀算」というやつに似ています。 中学校では、「方程式」として習いました。 しかし、方程式は２つの条件式(この問題の場合)が必要ですが、１つしか 与えられていません。どうすればいいのでしょうか？ すべての組み合わせを探せばいいのです。人間が計算でやると大変ですが、 コンピュータはこのような単純作業の繰り返しは得意です。


4章:分岐構造

4-8.練習問題

  問題：
50円切手と、80円切手を買いに、 東急ストアに行きました。 所持金はキーボードから入力された額です。でもお釣りをもらうのは面倒なので、 所持金ぴったりの額で買いたいです。何枚ずつ買えば良いでしょう？

  ヒント：
これは小学校の時にやった「鶴亀算」というやつに似ています。 中学校では、「方程式」として習いました。 しかし、方程式は２つの条件式(この問題の場合)が必要ですが、１つしか 与えられていません。どうすればいいのでしょうか？ すべての組み合わせを探せばいいのです。人間が計算でやると大変ですが、 コンピュータはこのような単純作業の繰り返しは得意です。

void main(){
    int x, y, shojikin;

    printf("所持金は?\n");

    x=0;
    y=0;

    while(80*x + 50*y <= shojikin){
        while(80*x + 50*y <= shojikin){
            if(80*x + 50*y == shojikin)
                printf("80円切手%d枚、50円切手%d枚\n",x,y);
            y++;
        }
        x++;
        y=0;
    }
}


4章:分岐構造

4-9.練習問題

  問題：
4桁以内の正の整数を2つ以上入力し、入力された数の最大値、最小値を求め、 さらに何件目の入力かを表示せよ。ただし5桁以上もしくは負の数が入力 されたら終了とする。


4章:分岐構造

4-9.練習問題

  問題：
4桁以内の正の整数を2つ以上入力し、入力された数の最大値、最小値を求め、 さらに何件目の入力かを表示せよ。ただし5桁以上もしくは負の数が入力 されたら終了とする。

void main(){
    int imax, imin, kmax, kmin, k, i;

    imax=-1;
    imin=10000;
    kmax=0;
    kmin=0;
    k=0;

    do{
        scanf("%d",i);

        if(0<i && i<10000){
            k++;
            if(imax9999);

    if(k==0)
        printf("何も入力してないじゃん\n");
    else{
        printf("最大値 %d (%d回目の入力)\n",imax,kmax);
        printf("最小値 %d (%d回目の入力)\n",imin,kmin);
    }
}


5章:配列

5-1.配列とは

    例えば100個のデータがあった場合、これを入れるための変数として、a1,a2,a3,・・・,a100と 変数を設定したのでは大変なことになってしまうことは明らかです。このデータの形式が 同じ場合、メモリー上に100個の連続した領域を確保し、何件ものデータを総括的に扱う ことができる方法を配列といいます。配列は、変数名と何番目のデータを指すかという 添え字で構成されています。

    例えば、１ヶ月のお店の来客数を管理するのに、raikyaku01,raikyaku02, raikyaku03,・・・raikya31なんてやっていたら大変なことになります。 そこでraikyakuという配列を日にちという添え字を用いて利用するわけです。具体的には raikyaku(1),raikyaku(2),・・・raikyaku(31)ってな具合です。

    配列を利用すると何が便利なのでしょうか？　それは添え字に変数が使えることです。 先ほどの１ヶ月の来客数を表示する場合、配列を使わないとraikyaku01, raikyaku02,raikyaku03,・・・raikya31と表示させたい変数を すべて列挙しなくてはなりません。配列を使うと、反復構造を使って、添え字の変数を 1ずつ増やしていくだけでいいのです。

    ところで、配列の添え字は0から始まる場合と、1から始まる 場合があります。 C言語では0から始まります。これに対してCOBOLでは1から始まります。このページでは 主にC言語を使っていますので、0から始まることにします。また、C言語では変数を 使う前に、領域を確保しなければなりません。配列の場合も何番まで使うかをあらかじめ 宣言しなければなりません。宣言した以上の値を添え字 として用いた場合、他のデータ領域を破壊することになりかねません。 この場合どこに影響があるかわかりません。最悪の場合、プログラムが暴走する可能性も ありまし、OS自体がおかしくなり、リセットする可能性もあります。この点はしっかりと 注意してください。

    ちなみに、C言語での配列使用宣言は下記のようにします。
int raikyaku[10];
  宣言のみ
  [10]は配列の大きさ(0から9の10個分確保する)
char a[]={10,20,30,40,50}
  宣言　＋　数の代入
  この場合配列の大きさは省略できる
  省略した場合は代入を行った5つ分を確保する

    C言語ではraikyaku[10]のように添え字を大括弧で表現しますが、フローチャート ではraikyaku(10)のように普通の括弧で表現します。

       また、C言語では配列を使うと宣言しただけでは、変数と同様何が入っているかわかりません。 つまり使う前に配列の中身をきれいにしなくてはなりません。


5章:配列

例題

    曜日ごとの来客数を配列に格納し、曜日番号を入力するとその曜日の来客数は画面に出力 せよ。ただし、曜日番号に9を入力すると終了する。また、曜日番号は下記の通りとする。
日曜日・・・0
月曜日・・・1
火曜日・・・2
水曜日・・・3
木曜日・・・4
金曜日・・・5
土曜日・・・6
  下記のフローチャートの続きを書け。


5章:配列

例題

    曜日ごとの来客数を配列に格納し、曜日番号を入力するとその曜日の来客数は画面に出力 せよ。ただし、曜日番号に9を入力すると終了する。また、曜日番号は下記の通りとする。
日曜日・・・0
月曜日・・・1
火曜日・・・2
水曜日・・・3
木曜日・・・4
金曜日・・・5
土曜日・・・6

void main(){
    int a;
    int raikyaku[7]={521,195,221,329,352,399,450};

    /*　ここらか記入せよ /*

}


5章:配列

例題

    曜日ごとの来客数を配列に格納し、曜日番号を入力するとその曜日の来客数は画面に出力 せよ。ただし、曜日番号に9を入力すると終了する。また、曜日番号は下記の通りとする。
日曜日・・・0
月曜日・・・1
火曜日・・・2
水曜日・・・3
木曜日・・・4
金曜日・・・5
土曜日・・・6

void main(){
    int a;
    int raikyaku[7]={521,195,221,329,352,399,450};

    scanf("%d",&a);
    while(a!=9){
        if(A<0 || a>6)
            printf("error\n");
        else
            printf("%d人\n",raikyaku[a]);
        scanf("%d",&a);
    }
}


5章:配列

5-2.練習問題

    曜日ごとの来客数を配列に格納し、１週間の合計と平均を求めよ。
    下記のフローチャートの続きを書け。


5章:配列

5-2.練習問題

    曜日ごとの来客数を配列に格納し、１週間の合計と平均を求めよ。
    下記のフローチャートの続きを書け。

void main(){
    int a;
    int raikyaku[7]={521,195,221,329,352,399,450};

    /*　ここらか記入せよ /*

}


5章:配列

5-2.練習問題

    曜日ごとの来客数を配列に格納し、１週間の合計と平均を求めよ。
    下記のフローチャートの続きを書け。

void main(){
    int t, i, av;
    int raikyaku[7]={521,195,221,329,352,399,450};

    t=0;
    i=0;

    do{
        t=t+raikyaku[i];
        i++;
    }while( i < 7 );

    av=t / i;
    printf("合計 : %d人\n平均 : %d人\n",t, av);

}


5章:配列

5-3.２次配列

    今まで添え字は一種類だけ、すなわち１次配列のみ扱ってきた。１次配列という言葉が あるのだから当然２次配列という言葉もある。２次配列という言葉があるのだから、 当然２次配列という概念もある。ここでは２次配列について解説する。

    例えば、先ほどは１店舗の１週間の来客数を見てきた。しかし店舗数が増えれば、 ２次配列を使うのが便利である。ここでは、いろいろな例を出したいので、趣向を変えて 成績表を考えてみる。

    下記のような成績表があったとする。このような成績表は、生徒(出席番号)と科目という ２つの要素から成り立っている。つまり２つの添え字があれば、点数を読み出せることになる。

出席番号 | 氏　名 | 国語 | 数学 | 理科 | 社会 | 英語
1 | 荒垣人絵 | 65 | 71 | 74 | 82 | 97
2 | 今井里絵子 | 84 | 83 | 74 | 64 | 65
3 | 下原多香子 | 74 | 74 | 78 | 80 | 78
4 | 嶋袋寛子 | 90 | 90 | 90 | 90 | 90
5 | 鈴木亜美 | 87 | 78 | 85 | 60 | 88
6 | 広末良子 | 93 | 95 | 94 | 92 | 95
    登場する人物は架空のものであり、実在しません。

  まずこのような表を作る場合、C言語では１次配列と同様、宣言をしなくてはなりません。
int seiseki[6][5];
    宣言のみ
    [6][5]は配列の大きさ
    (縦6人分、横5つ分確保する)
int seiseki[][5]={{65,71,74,82,97},
                  {84,83,74,64,65},
                  {74,74,78,80,78},
                  {90,90,90,90,90},
                  {87,78,85,60,88},
                  {93,95,94,92,95}};
    宣言　＋　代入
    この場合配列の大きさ(縦=人数)は省略できる
    (ただしコンパイラにもよるようです)


5章:配列

5-4.練習問題

  問題：
次の成績表を出力せよ。また各人毎に合計点と平均点を表示せよ。 ただし、配列の内容は前のページのものをそのまま使う。

  ヒント：
出席番号は1から始まるが、配列は0から始まることに注意する

出席番号 国語 数学 理科 社会 英語 合計点 平均点
1 65 71 74 82 97 - -
2 84 83 74 64 65 - -
3 74 74 78 80 78 - -
4 90 90 90 90 90 - -
5 87 78 85 60 88 - -
6 93 95 94 92 95 - -


5章:配列

5-4.練習問題

  問題：
次の成績表を出力せよ。また各人毎に合計点と平均点を表示せよ。 ただし、配列の内容は前のページのものをそのまま使う。

  ヒント：
出席番号は1から始まるが、配列は0から始まることに注意する

出席番号 国語 数学 理科 社会 英語 合計点 平均点
1 65 71 74 82 97 - -
2 84 83 74 64 65 - -
3 74 74 78 80 78 - -
4 90 90 90 90 90 - -
5 87 78 85 60 88 - -
6 93 95 94 92 95 - -

void main(){
    int seiseki[][5]={{65,71,74,82,97}
            {84,83,74,64,65},
            {74,74,78,80,78},
            {90,90,90,90,90},
            {87,78,85,60,88},
            {93,95,94,92,95}};
    int i, j, t, av;
    printf("番号　国語　数学　理科　社会　英語　合計　平均\n");

    /* ここに記入せよ /*

}


5章:配列

5-4.練習問題

  問題：
次の成績表を出力せよ。また各人毎に合計点と平均点を表示せよ。 ただし、配列の内容は前のページのものをそのまま使う。

  ヒント：
出席番号は1から始まるが、配列は0から始まることに注意する

出席番号 国語 数学 理科 社会 英語 合計点 平均点
1 65 71 74 82 97 - -
2 84 83 74 64 65 - -
3 74 74 78 80 78 - -
4 90 90 90 90 90 - -
5 87 78 85 60 88 - -
6 93 95 94 92 95 - -

void main(){
    int seiseki[][5]={{65,71,74,82,97}
            {84,83,74,64,65},
            {74,74,78,80,78},
            {90,90,90,90,90},
            {87,78,85,60,88},
            {93,95,94,92,95}};
    int i, j, t, av;
    printf("番号　国語　数学　理科　社会　英語　合計　平均\n");

    i=0;
    do{
        t=0;
        j=0;
        do{
            printf("%4d  ",seiseki[i][j]);
            t=t+seiseki[i][j];
            j++;
        }while(j<=4);

        av=t/j;
        printf("%6d%6d\n",t,av);

        i++;
    }while(i<=5);
}


5章:配列

5-5.素数を求める,その1

  問題：
100以下の素数を求めよ。　タイトルそのまま・・・(^_^;)

    素数とは、1より大きく、1とその数以外に約数を持たない自然数である。すなわち 一番小さい素数は2である。ここでは次のアルゴリズムによって素数を求める。

    今、一番小さい素数2がs(0)に入っている。
  • 3はそれより小さい素数s(0)=2で割り切れないので、素数でありこれをs(1)に代入する。
  • 4はそれより小さい素数s(0)=2で割り切れるので、素数ではない。
  • 5はそれより小さい素数s(0)=2、s(1)=3でも割り切れないので素数であり、s(2)に代入する。
  • 6はそれより小さい素数s(0)=2で割り切れるので、素数ではない。
  • 　　　　　　　　　　：
  • 　　　　　　　　　　：
  • ある数nは、それより小さい素数s(0),s(1),s(2),・・・,s(i)で割り切れなければ素数であり、s(i+1)に代入する。

    偶数は素数でないことを利用して、3から5,7,9,・・・と奇数のみを考えていく。


5章:配列

5-5.素数を求める,その1

  問題：
100以下の素数を求めよ。　タイトルそのまま・・・(^_^;)

    今、一番小さい素数2がs(0)に入っている。
  • 3はそれより小さい素数s(0)=2で割り切れないので、素数でありこれをs(1)に代入する。
  • 4はそれより小さい素数s(0)=2で割り切れるので、素数ではない。
  • 5はそれより小さい素数s(0)=2、s(1)=3でも割り切れないので素数であり、s(2)に代入する。
  • 6はそれより小さい素数s(0)=2で割り切れるので、素数ではない。
  • 　　　　　　　　　　：
  • 　　　　　　　　　　：
  • ある数nは、それより小さい素数s(0),s(1),s(2),・・・,s(i)で割り切れなければ素数であり、s(i+1)に代入する。

    偶数は素数でないことを利用して、3から5,7,9,・・・と奇数のみを考えていく。


5章:配列

5-5.素数を求める,その1

  問題：
100以下の素数を求めよ。　タイトルそのまま・・・(^_^;)

    今、一番小さい素数2がs(0)に入っている。
  • 3はそれより小さい素数s(0)=2で割り切れないので、素数でありこれをs(1)に代入する。
  • 4はそれより小さい素数s(0)=2で割り切れるので、素数ではない。
  • 5はそれより小さい素数s(0)=2、s(1)=3でも割り切れないので素数であり、s(2)に代入する。
  • 6はそれより小さい素数s(0)=2で割り切れるので、素数ではない。
  • 　　　　　　　　　　：
  • 　　　　　　　　　　：
  • ある数nは、それより小さい素数s(0),s(1),s(2),・・・,s(i)で割り切れなければ素数であり、s(i+1)に代入する。

    偶数は素数でないことを利用して、3から5,7,9,・・・と奇数のみを考えていく。

void main(void){
    int i,j,sho,amari,n;
    int s[100];

    i=0;
    while(i!=50){
        s[i]=0;
        i++;
    }
    i=1;
    s[0]=2;

    n=3;
    j=0;

    while(n<=100){
        sho=n/s[j];
        amari=n%s[j];

        if(amari==0){
            n=n+2;
            j=1;
        }
        else{
            if(sho>=s[j])
                j++;
            else{
                s[i]=n;
                i++;
                n=n+2;
                j=1;
            }
        }
    }

    i=0;
    while(s[i]!=0){
        printf("%8d",s[i]);
        i++;
    }
}


5章:配列

5-5.素数を求める,その1

  問題：
100以下の素数を求めよ。　タイトルそのまま・・・(^_^;)

    今、一番小さい素数2がs(0)に入っている。
  • 3はそれより小さい素数s(0)=2で割り切れないので、素数でありこれをs(1)に代入する。
  • 4はそれより小さい素数s(0)=2で割り切れるので、素数ではない。
  • 5はそれより小さい素数s(0)=2、s(1)=3でも割り切れないので素数であり、s(2)に代入する。
  • 6はそれより小さい素数s(0)=2で割り切れるので、素数ではない。
  • 　　　　　　　　　　：
  • 　　　　　　　　　　：
  • ある数nは、それより小さい素数s(0),s(1),s(2),・・・,s(i)で割り切れなければ素数であり、s(i+1)に代入する。

    偶数は素数でないことを利用して、3から5,7,9,・・・と奇数のみを考えていく。

void main(){
    int i,j,k,amari;
    int s[100];

    k=0;
    s[k]=2;
    i=0;
    j=3;

    while(j<=100){
        amari=j % s[i];
        if(amari==0){
            i=1;
            j=j+2;
        }
        else{
            i++;
            if(i>k){
                k=i;
                s[k]=j;
                i=1;
                j=j+2;
            }
        }
    }

    i=0;
    while(i<=k){
        printf("%8d",s[i]);
        i++;
    }
}


5章:配列

5-6.素数を求める,その2(エラトステネスのふるい)

  問題：
100以下の素数を求めよ。　たいとるそのまま・・・(^_^;)

    素数とは、1より大きく、1とその数以外に約数を持たない自然数である。すなわち 一番小さい素数は2である。ここでは「エラトステネスのふるい」という有名な アルゴリズムによって素数を求める。

エラトステネスのふるいとは、
  1. 100個の配列を用意する
  2. すべての配列の中身に1を代入する
  3. 2の倍数番目の中に0を代入する
  4. 3の倍数番目の中に0を代入する
  5. 4番目はすでに0なので、次に進む
  6. 5の倍数番目の中に0を代入する
  7. 6番目はすでに0なので、次に進む
  8. 7の倍数番目の中に0を代入する
  9. 　　　　：
  10. 　　　　：
  11. 100の平方根まで繰り返す

     例えば99を見たとき、100の平方根=10であるから、11でも割り切れるが、 11以下の数ですでにチェックされている。したがって100の平方根=10以上は チェックする必要がない。

  これをもう少し視覚的に見てみよう。
添字 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
中身 / / 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                          ↓
            2の倍数を消していきます。
                          ↓
添字 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
中身 / / 1 1 0 1 0 1 0 1 0 1 0 1 0 1 0
                          ↓
            3の倍数を消していきます。
                          ↓
添字 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
中身 / / 1 1 0 1 0 1 0 0 0 1 0 1 0 0 0
                          ↓
      4の倍数は2の倍数ですでに消えている。
          そこで5の倍数を消していきます。
                          ↓
添字 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
中身 / / 1 1 0 1 0 1 0 0 0 1 0 1 0 0 0
                          ↓
      6の倍数は2の倍数ですでに消えている。
          そこで7の倍数を消していきます。
                          ↓
                          ↓
      8の倍数は2の倍数ですでに消えている。
      9の倍数は3の倍数ですでに消えている。
      10の倍数は2の倍数ですでに消えている。
      100の平方根=10を越えたのでおしまい。
                          ↓
      配列の中身が1である添え字が素数である。


5章:配列

5-6.素数を求める,その2(エラトステネスのふるい)

  問題：
100以下の素数を求めよ。　たいとるそのまま・・・(^_^;)

    素数とは、1より大きく、1とその数以外に約数を持たない自然数である。すなわち 一番小さい素数は2である。ここでは「エラトステネスのふるい」という有名な アルゴリズムによって素数を求める。

エラトステネスのふるいとは、
  1. 100個の配列を用意する
  2. すべての配列の中身に1を代入する
  3. 2の倍数番目の中に0を代入する
  4. 3の倍数番目の中に0を代入する
  5. 4番目はすでに0なので、次に進む
  6. 5の倍数番目の中に0を代入する
  7. 6番目はすでに0なので、次に進む
  8. 7の倍数番目の中に0を代入する
  9. 　　　　：
  10. 　　　　：
  11. 100の平方根まで繰り返す

     例えば99を見たとき、100の平方根=10であるから、11でも割り切れるが、 11以下の数ですでにチェックされている。したがって100の平方根=10以上は チェックする必要がない。

  これをもう少し視覚的に見てみよう。
添字 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
中身 / / 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
                          ↓
            2の倍数を消していきます。
                          ↓
添字 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
中身 / / 1 1 0 1 0 1 0 1 0 1 0 1 0 1 0
                          ↓
            3の倍数を消していきます。
                          ↓
添字 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
中身 / / 1 1 0 1 0 1 0 0 0 1 0 1 0 0 0
                          ↓
      4の倍数は2の倍数ですでに消えている。
          そこで5の倍数を消していきます。
                          ↓
添字 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
中身 / / 1 1 0 1 0 1 0 0 0 1 0 1 0 0 0
                          ↓
      6の倍数は2の倍数ですでに消えている。
          そこで7の倍数を消していきます。
                          ↓
                          ↓
      8の倍数は2の倍数ですでに消えている。
      9の倍数は3の倍数ですでに消えている。
      10の倍数は2の倍数ですでに消えている。
      100の平方根=10を越えたのでおしまい。
                          ↓
      配列の中身が1である添え字が素数である。

void main(){
    int i,j;
    char s[101];

    i=2;
    while(i<=100){
        s[i]=1;
        i++;
    }

    i=2;

    while(i*i<=100){
        j=2;

        while(i*j<=100 && s[i]!=0){
            s[i*j]=0;
            j++;
        }
        i++;
    }

    i=2;

    while(i<=100){
        if(s[i]==1)
            printf("%8d",s[i]);
        i++;
    }
}


6章:検索・ソート

    検索、ソートはよく使われる手法である。「検索」「ソート」といった特別のフローチャート の書き方があるわけではない。今までの反復処理や配列、分岐構造を組み合わせただけである。 しかし、ソートの方法はいろいろなアルゴリズムがあり、その場に一番ふさわしいものを選ぶ 必要がある。(ホントか?)　というわけで、この章ではソートの基本である 検索を紹介し、その後、ソートのアルゴリズムを紹介する。

6-1.サブルーチン

    サブルーチン（言語によってはモジュールとも言う）とは、同じ処理を何回も繰り返す処理を ひとまとめにしておくことによって、アルゴリズムやプログラムを何度も書く手間を省くことや 他のプログラムを作る際に、同じアルゴリズムやプログラムを再利用することで、バグが出る危 険性を最小限に押さえることを目的としている。

  • 同じ処理を何度も呼び出す際に、同じことを何度も書く手間を省く
  • 同じ処理を他のプログラムで書く際に、既存のアルゴリズムやプログラムを再利用するこ とで、バグの発生を押さえる
        →能率、信頼性の向上

  サブルーチン（モジュール）を作る際の注意点を以下にあげる。

  • 基本的に１つのサブルーチンには１つの機能しか書かない
  • サブルーチンの入り口と出口はそれぞれ１つ
  • サブルーチンの名前は一目で機能がわかるように命名する
  • 「Aというサブルーチンを呼び出すためにはBというサブルーチンを呼び出していることが前提」 というのは避ける

・サブルーチンの書き方
    サブルーチンの入り口にその名前、出口に「exit」と書く。

・サブルーチンの呼び出し方


6章:検索・ソート

6-2.シーケンシャルサーチ

    検索とは、目的のデータを探し出すことである。シーケンシャルサーチとは検索のアルゴリズムの 一種で、もっとも単純なアルゴリズムである。すなわち最初から（もしくは最後から）１つ１つ 探していく方法である。この方法は単純でわかりやすい反面、照合回数が多く効率が悪い。 したがって、データ数が少ない場合に使用される。

例）　n個の数字が格納されている配列cdがある。変数aと同じ数字が格納されているのは何番目かを 変数pに求めよ。


6章:検索・ソート

6-2.例題

    前ページの方法では、目的のデータが見つかっても最後まで検索を続ける。そこで、 見つかったら検索を終了するように改良せよ。


6章:検索・ソート

6-3.バイナリーサーチ

    「数当てゲームです。1から20までのある数字があります。これを当ててください。ただし、 答えた数よりも大きいか、小さいかは教えてあげます。」と言われた場合、どうすれば 答える回数が少なくてすむでしょうか？

例）　答えが7の場合
  1. 10?　・・・　小さい
  2. ５？　・・・　大きい
  3. ７？　・・・　正解

    と答えるのがベストです。どういう根拠でしょう？

・まずはじめは、1～20の範囲がありますので、その中間の10を言ってみます

・10よりも小さいので1～9の範囲に縮まりました

・そこで1～9の中間の5を言ってみます

・5よりも大きいので6～9の範囲になりました

・その中間の7にしたところ、正解でした

    この方法は、上限、下限の範囲を狭めていく方法で、１回の質問（照合）で 範囲が半分になる。したがって要素数が1000の場合、その平均照合回数は log21000≒10回と、先に述べた、シーケンシャルサーチの 1000÷2≒500回と比べてかなり効率がよいことがわかる。

    ただし、データが昇順に並んでいないと検索できないという欠点がある。


6章:検索・ソート

6-4.練習問題

    下記のような表がある。所持金を入力すると氏名を表示し、該当者がいなければ "該当者なし"と表示するプログラムを作りたい。ただし、表は「tbl_set」という サブルーチンが用意され、全体の流れも作成済みであるので、シーケンシャルサーチと バイナリーサーチのサブルーチンを作れ。

氏名 所持金
阿部なつみ
新垣仁恵
今井理恵子
上原多佳子
奥菜恵美
嶋袋寛子
鈴木アミ
広末良子 		120
240
290
320
450
600
630
930

void tbl_set()
{
    name[0]="阿部なつみ";
    name[1]="新垣仁恵";
    name[2]="今井理恵子";
    name[3]="上原多佳子";
    name[4]="奥菜恵美";
    name[5]="嶋袋寛子";
    name[6]="鈴木アミ";
    name[7]="広末良子";

    syoji[0]=120;
    syoji[1]=240;
    syoji[2]=290;
    syoji[3]=320;
    syoji[4]=450;
    syoji[5]=600;
    syoji[6]=630;
    syoji[7]=930;

    n=8;
}

#include  <stdio.h>

void  tbl_set();
void  seq_search();

int a,i,p,l,h;
int n=8;
int syoji[9];
char *name[11];

void main()
{
    tbl_set();

    scanf("%d",&a);

    seq_search();
    if(p==-1){
        printf("該当者なし\n");
    }
    else{
        printf("%2d%s\n",p,name[p]);
    }
}


6章:検索・ソート

6-4.練習問題

    下記のような表がある。所持金を入力すると氏名を表示し、該当者がいなければ "該当者なし"と表示するプログラムを作りたい。ただし、表は「tbl_set」という サブルーチンが用意され、全体の流れも作成済みであるので、シーケンシャルサーチと バイナリーサーチのサブルーチンを作れ。

氏名 所持金
阿部なつみ
新垣仁恵
今井理恵子
上原多佳子
奥菜恵美
嶋袋寛子
鈴木アミ
広末良子 		120
240
290
320
450
600
630
930

void tbl_set()
{
    name[0]="阿部なつみ";
    name[1]="新垣仁恵";
    name[2]="今井理恵子";
    name[3]="上原多佳子";
    name[4]="奥菜恵美";
    name[5]="嶋袋寛子";
    name[6]="鈴木アミ";
    name[7]="広末良子";

    syoji[0]=120;
    syoji[1]=240;
    syoji[2]=290;
    syoji[3]=320;
    syoji[4]=450;
    syoji[5]=600;
    syoji[6]=630;
    syoji[7]=930;

    n=8;
}

#include  <stdio.h>

void  tbl_set();
void  seq_search();

int a,i,p,l,h;
int n=8;
int syoji[9];
char *name[11];

void main()
{
    tbl_set();

    scanf("%d",&a);

    seq_search();
    if(p==-1){
        printf("該当者なし\n");
    }
    else{
        printf("%2d%s\n",p,name[p]);
    }
}

解答（バイナリサーチ）

解答（シーケンシャルサーチ）


6章:検索・ソート

6-4.練習問題

    下記のような表がある。所持金を入力すると氏名を表示し、該当者がいなければ "該当者なし"と表示するプログラムを作りたい。ただし、表は「tbl_set」という サブルーチンが用意され、全体の流れも作成済みであるので、シーケンシャルサーチと バイナリーサーチのサブルーチンを作れ。

氏名 所持金
阿部なつみ
新垣仁恵
今井理恵子
上原多佳子
奥菜恵美
嶋袋寛子
鈴木アミ
広末良子 		120
240
290
320
450
600
630
930

解答（バイナリサーチ）
#include  <stdio.h>
#include  <conio.h>

void  tbl_set();
void  bin_search();

int a,i,p,l,h;
int n=8;
int syoji[9];
char *name[9];

void main()
{
     tbl_set();

     scanf("%d",&a);

     bin_search();
     if(p==-1)
         printf("該当者なし\n");
     else
         printf("%s\n",name[p]);
}

void bin_search()
{
    p=-1;
    l=0;
    h=n-1;

    while (l <= syoji[(l+h)/2] && p==-1){
        if (a < syoji[(l+h)/2])
            l = (l + h) / 2 + 1;

        else{
            if (a == syoji[(l+h)/2])
                p = (l + h) / 2;
            else
            h = (l + h) / 2 - 1;
        }
    }
}

void tbl_set()
{
    name[0]="阿部なつみ";
    name[1]="新垣仁恵";
    name[2]="今井理恵子";
    name[3]="上原多佳子";
    name[4]="奥菜恵美";
    name[5]="嶋袋寛子";
    name[6]="鈴木アミ";
    name[7]="広末良子";

    syoji[0]=120;
    syoji[1]=240;
    syoji[2]=290;
    syoji[3]=320;
    syoji[4]=450;
    syoji[5]=600;
    syoji[6]=630;
    syoji[7]=930;

    n=8;
}

解答（シーケンシャルサーチ）
#include  <stdio.h>
#include  <conio.h>

void  tbl_set();
void  bin_search();

int a,i,p,l,h;
int n=8;
int syoji[9];
char *name[9];

void main()
{
     tbl_set();

     scanf("%d",&a);

     seq_search();
     if(p==-1)
         printf("該当者なし\n");
     else
         printf("%s\n",name[p]);
}

void seq_search()
{
    i=0;
    p=-1;
    while (i < n && p==-1){
        if (a == name[i])
            p=i;
        else
            i++;
    }
}

void tbl_set()
{
    name[0]="阿部なつみ";
    name[1]="新垣仁恵";
    name[2]="今井理恵子";
    name[3]="上原多佳子";
    name[4]="奥菜恵美";
    name[5]="嶋袋寛子";
    name[6]="鈴木アミ";
    name[7]="広末良子";

    syoji[0]=120;
    syoji[1]=240;
    syoji[2]=290;
    syoji[3]=320;
    syoji[4]=450;
    syoji[5]=600;
    syoji[6]=630;
    syoji[7]=930;

    n=8;
}


6章:検索・ソート

6-5.ルックアップテーブル

    検索する際、必ずしも「１：１」の検索とは限らない。例えば、整理番号が1～100の人は401会場、 101～200の人は302会場・・・といった場合など、ある範囲指定に入っているかどうかを調べたい 場合もある。このような場合、１：１のテーブルを作っていたら膨大なデータ量になってしまう。 そこで、範囲の上限（場合によっては下限）のみの表を作り、その値以下（以上）であるか どうかを判断する。（下図参照）　このようなテーブルの参照方式をルックアップ方式と 言う。

    このような場合のアルゴリズムを下図に示す。

#include  <stdio.h>
#include  <conio.h>

void  tbl_set();

int a,i,n;
int Q[5];
int R[5];

void main()
{
    tbl_set();

    scanf("%d",&a);

    while (a > 0){

        i=n-1;
        x=-1;

        while ( i != -1){

            if( a >=Q [i])
                i = -1;
            else
                i--;
            printf("整理番号%a番の方は %d会場です\n", a, R[i]);
        }
    scanf("%d",&a);
    }
}

void tbl_set(){
    Q[0] = 100;
    Q[1] = 200;
    Q[2] = 250;
    Q[3] = 300;
    Q[4] = 360;

    R[0] = 401;
    R[1] = 302;
    R[2] = 101;
    R[3] = 102;
    R[4] = 301;

    n=5;
}


6章:検索・ソート

6-6.ソートとは

    ソートとは、データをある順序に並び替えることである。一般には、
  • 昇順（小さな値　→　大きな値）と
  • 降順（大きな値　→　小さな値）である。

    このソートの目的は色々あるが、例えば、試験の結果の順位を出したり、 住所録を郵便番号順にするといった用途がある。

    さて、ソートの方法も検索と同様に様々な考え方があるが、本ホームページでは 次の３つの方法について考えてみることにする。

  • 選択法　・・・・・・・・　もっとも単純な方法
  • 交換法　・・・・・・・・　選択法を少し改良したもの
  • バブルソート　・・・　交換法を改良したもの
  • 挿入法　・・・・・・・・　？？？
  • クイックソート　・・・・　一番速いソート法


6章:検索・ソート

6-7.選択法

    あなたが下の表を昇順に並び替えろと言われたとき、どのようにしますか？
  1. 一番小さい値を探す
  2. それを一番上に書く
  3. チェック済みだよと言う意味で、その値を消す（チェックする）
  4. これを要素数分繰り返す
    ほとんどの人がこのような考え方をすると思います。選択法とは、まさしく その通りに実行する方法です。
|　　要素　　| 　　ソート前　　| 　　ソート後　　|
0 | 84 | 43
1 | 121 | 58
2 | 43 | 83
3 | 93 | 84
4 | 140 | 93
5 | 83 | 93
6 | 14 | 93
7 | 93 | 121
8 | 181 | 140
9 | 58 | 181
    しかしプログラム上では消す（チェックする）ということはできないので、 消す（チェックする）かわりに、あり得ない数字（データの内容によって違うが、 負の数、０あるいは9999など）を代入する。ここでは999を 用いることにする。


6章:検索・ソート

|　　要素　　| 　　ソート前　　| 　　ソート後　　|
0 | 84 | 43
1 | 121 | 58
2 | 43 | 83
3 | 93 | 84
4 | 140 | 93
5 | 83 | 93
6 | 14 | 93
7 | 93 | 121
8 | 181 | 140
9 | 58 | 181
  これを昇順にソートするアルゴリズムを下記に示す。

#include <stdio.h>

void init();
void seq_sort();
void disp();

int	A[10];
int	B[10];
int	n;

int main(int argc, char* argv[]){
    init();
    seq_sort();
    disp();
    return 0;
}

void init(){
    A[0] =  84;
    A[1] = 121;
    A[2] =  43;
    A[3] =  93;
    A[4] = 140;
    A[5] =  83;
    A[6] =  14;
    A[7] =  93;
    A[8] = 181;
    A[9] =  58;
    n    =  10;
}
void seq_sort(){
    int	i = 0;
    for ( ; i < n ; i++ ){
        int	k;
        int a = 999;
        int	j = 0;
        for ( ; j < n ; j++ ){
            if ( a>A[j] ){
                a = A[j];
                k = j;
            }
        }
        A[k] = 999;
        B[i] = a;
    }
}
void disp(){
    int i = 0;
    for ( ; i < n ; i++ )
        printf("%d   ",B[i]);
    printf("\n");
}


6章:検索・ソート

6-8.交換法

    選択法では、要素数の２乗回比較するため効率が悪く、さらにソート前のAという 配列とソート後のBという配列が必要になるため不便である。

    そこで選択法を少し発展させて、一番小さい値を消す（チェックする）のをやめ、 その選択された値と、それが入る順番の要素データを交換しながら処理することにする。 （下図参照）

    こうすることにより、最小値を選び出す範囲が、「要素数、要素数-1、 要素数-2、・・・」と減っていくために、比較回数が半分となり処理速度が向上する。 さらに配列は１つで良いため、メモリー、手間の観点から言っても効率が良くなる。

|　要素　| 　ソート前　| 1回処理後　| 2回処理後　| 3回処理後　| 4回処理後　|
0 | 84 | 14 | 14 | 14 | 14
1 | 121 | 121 | 43 | 43 | 43
2 | 43 | 43 | 121 | 58 | 58
3 | 93 | 93 | 93 | 93 | 83
4 | 140 | 140 | 140 | 140 | 140
5 | 83 | 83 | 83 | 83 | 93
6 | 14 | 84 | 84 | 84 | 84
7 | 93 | 93 | 93 | 93 | 93
8 | 181 | 181 | 181 | 181 | 181
9 | 58 | 58 | 58 | 121 | 121
        赤字：前回と値が変わったところ 　　　　青字：検索範囲の最小値
    これを昇順にソートするアルゴリズムを考えてみてください。 ただし、メインルーチン、初期設定ルーチンは前ページの ものをそのまま使い、表示ルーチンはB(i)を表示していたものをA(i)を表示するように 変更するだけでよい。


6章:検索・ソート

|　要素　| 　ソート前　| 1回処理後　| 2回処理後　| 3回処理後　| 4回処理後　|
0 | 84 | 14 | 14 | 14 | 14
1 | 121 | 121 | 43 | 43 | 43
2 | 43 | 43 | 121 | 58 | 58
3 | 93 | 93 | 93 | 93 | 83
4 | 140 | 140 | 140 | 140 | 140
5 | 83 | 83 | 83 | 83 | 93
6 | 14 | 84 | 84 | 84 | 84
7 | 93 | 93 | 93 | 93 | 93
8 | 181 | 181 | 181 | 181 | 181
9 | 58 | 58 | 58 | 121 | 121
        赤字：前回と値が変わったところ 　　　　青字：検索範囲の最小値
  これを昇順にソートするフローチャートとＣ言語を下記に示す。

#include <stdio.h>

void init();
void seq_sort2();
void disp();

int	A[10];
int	n;

int main(int argc, char* argv[]){
    init();
    seq_sort2();
    disp();
    return 0;
}

void init(){
    A[0] =  84;
    A[1] = 121;
    A[2] =  43;
    A[3] =  93;
    A[4] = 140;
    A[5] =  83;
    A[6] =  14;
    A[7] =  93;
    A[8] = 181;
    A[9] =  58;
    n    =  10;
}
void seq_sort2(){
    int	i = 0;
    for ( ; n-1 > i ; i++ ){
        int	j = i + 1;
        int a = A[i];
        int k = i;
        for ( ; n > j ; j++ ){
            if ( a>A[j] ){
                a = A[j];
                k = j;
            }
        }
        A[k] = A[i];
        A[i] = a;
    }
}
void disp(){
    int i = 0;
    for ( ; i < n ; i++ )
        printf("%d   ",A[i]);
    printf("\n");
}


6章:検索・ソート

6-9.バブルソート

    さらに比較回数を少なくするために次のようなアルゴリズムでソートしてみる。 ただし、昇順ばかりではつまらないので、今度は降順にソートしてみる。

  1. 先頭(要素0番)から下に隣接する値を比較し、降順になっていなければその値を交換する。
  2. これをすべて降順になるまで繰り返す。

|　要素　| 　ソート前　| 1回処理後　| 2回処理後　| 3回処理後　| 4回処理後　|
0 | 84 | 121 | 121 | 121 | 140
1 | 121 | 84 | 93 | 140 | 121
2 | 43 | 93 | 140 | 93 | 93
3 | 93 | 140 | 84 | 84 | 93
4 | 140 | 83 | 83 | 93 | 181
5 | 83 | 43 | 93 | 181 | 84
6 | 14 | 93 | 181 | 83 | 83
7 | 93 | 181 | 58 | 58 | 58
8 | 181 | 58 | 43 | 43 | 43
9 | 58 | 14 | 14 | 14 | 14
        赤字：降順として確定したところ 　　　　青字：次に下に降りる値
１回目の処理は次のように行われる。
  • 84と121を比較すると降順になっていないので、交換する。
  • 84と43を比較すると降順になっているので、次に行く。
  • 43と93を比較すると降順になっていないので、交換する。
  • 43と140比較すると降順になっていないので、交換する。
  • 43と93比較すると降順になっていないので、交換する。
  • 43と14を比較すると降順になっているので、次に行く。
  • 14と93を比較すると降順になっていないので、交換する。
  • 14と181を比較すると降順になっていないので、交換する。
  • 14と58を比較すると降順になっていないので、交換する。
            →ある値がそれ以下の値に出会うまで、その値を下におろす

    このように降順にソートするフローチャートを考えてみてください。 ただし、メインルーチン、初期設定ルーチン、表示ルーチンは 前ページのものをそのまま使う。

    バブルソートとは、昇順に並べ替えたときに、小さい値が泡のように上に行くような 処理であるため、バブルソートという名が付けられた。


6章:検索・ソート

| 要素 | | ソート前 | | 1回処理後 | | 2回処理後 | | 3回処理後 | | 4回処理後 |
0 | 84 | 121 | 121 | 121 | 140
1 | 121 | 84 | 93 | 140 | 121
2 | 43 | 93 | 140 | 93 | 93
3 | 93 | 140 | 84 | 84 | 93
4 | 140 | 83 | 83 | 93 | 181
5 | 83 | 43 | 93 | 181 | 84
6 | 14 | 93 | 181 | 83 | 83
7 | 93 | 181 | 58 | 58 | 58
8 | 181 | 58 | 43 | 43 | 43
9 | 58 | 14 | 14 | 14 | 14
        赤字：降順として確定したところ 　　　　青字：次に下に降りる値

#include <stdio.h>

void init();
void babble();
void disp();

int	A[10];
int	n;

int main(int argc, char* argv[]){
    init();
    babble();
    disp();
    return 0;
}

void init(){
    A[0] =  84;
    A[1] = 121;
    A[2] =  43;
    A[3] =  93;
    A[4] = 140;
    A[5] =  83;
    A[6] =  14;
    A[7] =  93;
    A[8] = 181;
    A[9] =  58;
    n    =  10;
}
void babble(){
    int j    = n - 1;
    int flag = n;
    for ( ; j > 0 && flag != 0 ; j-- ){
        int i = 0;
        flag = 0;
        for ( ; j > i ; i++ ){
            if ( A[i+1] > A[i] ){
                int a = A[i+1];
                A[i+1] = A[i];
                A[i] = a;
                flag = 1;
            }
        }
    }
}
void disp(){
    int i = 0;
    for ( ; i < n ; i++ )
        printf("%d   ",A[i]);
    printf("\n");
}


6章:検索・ソート

6-10.挿入法

    次のようなアルゴリズムで、昇順にソートするフローチャートを考える。

  1. 整列済みの配列に、次のデータを挿入していくと考える。
  2. データの最後まで（次のデータがなくなるまで）繰り返す。

  この方法は、応用範囲が今までの方法と比べて高く、キーボードから値が入力されるたびに、 ソートすると言った処理などの場合、今までの方法だと入力されるたびにすべてをソートしな ければいけなかったのに対し、挿入法では１回の処理ですむ。
|　要素　| 　ソート前　| 1回処理後　| 2回処理後　| 3回処理後　| 4回処理後　|
0 | 84 | 84 | 43 | 43 | 43
1 | 121 | 121 | 84 | 84 | 84
2 | 43 | 43 | 121 | 93 | 93
3 | 93 | 93 | 93 | 121 | 121
4 | 140 | 140 | 140 | 140 | 140
5 | 83 | 83 | 83 | 83 | 83
6 | 14 | 14 | 14 | 14 | 14
7 | 93 | 93 | 93 | 93 | 93
8 | 181 | 181 | 181 | 181 | 181
9 | 58 | 58 | 58 | 58 | 58
        赤字：赤字の領域はソート済み 　　　　青字：赤字の領域に次に挿入する値

  すなわち赤字の領域はすでにソート済みの領域として、次に青地の値をその領域に 挿入する。このときにソートしながら挿入する。したがって、n個のデータをソートする 場合にはn回の処理が必要となる。

  このように降順にソートするフローチャートを考えてみてください。 ただし、メインルーチン、初期設定ルーチン、表示ルーチンは 今までののものをそのまま使う。


6章:検索・ソート

| 要素 | | ソート前 | | 1回処理後 | | 2回処理後 | | 3回処理後 | | 4回処理後 |
0 | 84 | 84 | 43 | 43 | 43
1 | 121 | 121 | 84 | 84 | 84
2 | 43 | 43 | 121 | 93 | 93
3 | 93 | 93 | 93 | 121 | 121
4 | 140 | 140 | 140 | 140 | 140
5 | 83 | 83 | 83 | 83 | 83
6 | 14 | 14 | 14 | 14 | 14
7 | 93 | 93 | 93 | 93 | 93
8 | 181 | 181 | 181 | 181 | 181
9 | 58 | 58 | 58 | 58 | 58
        赤字：赤字の領域はソート済み 　　　　青字：赤字の領域に次に挿入する値

#include <stdio.h>

void init();
void insert_sort();
void disp();

int	A[10];
int	n;

int main(int argc, char* argv[]){
    init();
    insert_sort();
    disp();
    return 0;
}

void init(){
    A[0] =  84;
    A[1] = 121;
    A[2] =  43;
    A[3] =  93;
    A[4] = 140;
    A[5] =  83;
    A[6] =  14;
    A[7] =  93;
    A[8] = 181;
    A[9] =  58;
    n    =  10;
}
void insert_sort(){
    int i = 1;
    while(i < n){
        int j = i;
        while(j != 0){
            if( A[j-1] > A[j] )
                j = 0;
            else{
                int a = A[j-1];
                A[j-1] = A[j];
                A[j] = a;
                j--;
            }
        }
        i++;
    }
}
void disp(){
    int i = 0;
    for ( ; i < n ; i++ )
        printf("%d   ",A[i]);
    printf("\n");
}


6章:検索・ソート

6-11.クイックソート

    次のようなアルゴリズムで、昇順にソートするフローチャートを考える。

  1. 配列中の要素の一番最初をピボットと名付ける
  2. 配列要素をピボットよりも小さいもの、ピボット、ピボットより大きいもの、に並び替える
    1. 配列要素の最初から、ピボットよりも小さいものを探す
    2. 配列要素の最後から、ピボットよりも大きいものを探す
    3. ピボットよりも小さいものが、ピボットよりも大きいものよりも後にあれば、ソート終了
    4. そうでなければ、その２つの要素を交換する
    5. この処理を、その続きの範囲から繰り返す
  3. ピボットよりも左側と、右側の領域で、この処理を再帰的に行う。

    このクイックソートは、今までに紹介した方法に比べて、処理速度が格段に速い。 JavaやMFCなどでは、標準関数として用意されている。
|　要素　| 　ソート前　| 1回　| 2回　| 3回　| 4回　| 5回　| 6回　| 7回　| 8回　| 9回　|
0 | 84 | 51 | 43 | 14 | 14 | 14 | 14 | 14 | 14 | 14
1 | 121 | 14 | 14 | 43 | 43 | 43 | 43 | 43 | 43 | 43
2 | 43 | 43 | 51 | 51 | 51 | 51 | 51 | 51 | 51 | 51
3 | 93 | 83 | 83 | 83 | 83 | 83 | 83 | 83 | 83 | 83
4 | 140 | 140 | 140 | 140 | 140 | 84 | 84 | 84 | 84 | 84
5 | 83 | 93 | 93 | 93 | 93 | 93 | 93 | 93 | 93 | 93
6 | 14 | 121 | 121 | 121 | 121 | 121 | 121 | 121 | 93 | 93
7 | 93 | 93 | 93 | 93 | 93 | 93 | 93 | 93 | 121 | 121
8 | 181 | 181 | 181 | 181 | 181 | 181 | 181 | 181 | 181 | 140
9 | 51 | 84 | 84 | 84 | 84 | 140 | 140 | 140 | 140 | 181
#include <stdio.h>
#define   LEN	10

void  display(int S[], int length);
int  *myQsort(int S[], int left, int right);

/*-----------------------  QuickSort
      クイックソート
      軸要素は，左端(数列の先頭)でとってある．

      Copyright by ECB.------------------------*/

//--------メイン関数------------------
void main(){
    int  S[LEN] = {84,121,43,93,140,83,14, 93, 181, 51}; //配列の初期値
    int *R;
    R = myQsort(S, 0, LEN-1); 		//クイックソート
    display(R, LEN);  //結果表示
}
//----------クイックソート--------------------
int *myQsort(int S[], int first, int last) {
    int i,j;
    int  x;
    x = S[first];			//ピボット
    i = first;			//first は 左境界，lastは 右境界
    j = last;
    while(true) {			//無限ループ
        int temp;
        while( S[i] < x )
            i++;		//交換する要素の決定
        while( x < S[j] )
            j--;
        if( i >= j )
            break;		//左境界が右境界を越えたら ループ終了
        temp = S[i];		//S[i] と S[j]を交換
        S[i] = S[j];
        S[j] = temp;
        i++;				//境界をさらにせばめる．
        j--;
    }
    display(S,LEN);			//途中経過

    if( first < i - 1 )
        myQsort( S, first, i-1 );  //S[i], S[j]の変換があれば もう一度行なう． <- ポイント
    if( j + 1 < last )
        myQsort( S, j+1, last );
    return S;
}

//-------結果表示---------------
void display(int S[], int length){
    for(int i = 0; i<length; i++)
        printf("%4d ",S[i]);
    printf("\n");
}


6章:検索・ソート

6-12.処理速度の比較

    今まで紹介したソート法５つについて、処理速度を比較してみたいと思います。 IE5.0など、Java Applet対応のブラウザを使用し、Java Appletが使用できるセキュリティー 状態にしてください。

選択法
バブルソート
挿入法

交換法
双方向バブルソート
クイックソート
    クリックするとソートを開始します。ある程度のところでウエイトをとっているので、 実際の処理速度とは若干異なりますが、一番遅い選択法と、一番速いクイックソートの差が 歴然としているのがお分かりいただけると思います。

    以前、「交換法は選択法の２倍の速さ」と解説しましたが、だいたいそのくらいの 差があることもお分かり頂けたかとおもいます。また、ソートの様子も視覚的に見ることができるの 感じがつかみやすいのではないかなぁ、と思います。


7章:文字列

7-1.文字と文字列

    1文字とは、1バイトの文字コードを言い、例えば'A'などとシングルクォーテーションで 囲んで表現する。文字列とは1文字以上の集まりを言い、"ABC"などとダブルクォーテーション で囲んで表現し、一般に配列に格納する。例えば、"A"という文字列と'A'という文字は 同じような感じがするが、これは別物である。

    また、文字列は複数の文字から形成されているため、何文字で構成されているかまたは、 メモリー上の（配列内の）どこまでが有効な文字なのかを管理しなければならない。 これはプログラミング言語によって異なる。

    このような管理の方法は大きく分けて以下の２種類に分かれる。

カウンタ方式　(PASCALなど)
文字列の長さを別の変数に持つ方法
  例：文字列A　'M' 'O' 'J' 'I' 'R' 'E' 'T' 'S' 'U'
      カウンタL　9

フラグ方式　(C言語、COBOLなど)
文字列の最後に、通常あり得ないコードを格納しておく方法
  例：文字列A 　'M' 'O' 'J' 'I' 'R' 'E' 'T' 'S' 'U' 0

   通常あり得ないコードとは、一般には「0」を用いる。C言語などではこれを「NULL」 と呼び、COBOLなどでは「LOW-VALUE」と呼ぶ。
  本ホームページでは、プログラミング言語にC言語を用いているので、フラグ方式を用いる。


7章:文字列

7-2.文字列処理

    文字列処理とは、読んで字のごとく「文字列を処理する」ことである。 例えば、文字列をコピーしたり、比較したり、結合したり、大文字を小文字へ変換したり などである。文字列のコピーなどC言語では、ある程度の文字列処理の標準関数を 用意しているが、ここでは基本的な文字列処理を作ってみることにする。

    文字列処理では文字数が何文字かわからないため、あらかじめ確保している配列の 容量を超えてしまうことがある。このような場合、C言語のコンパイル時にはエラーに ならないが、実行してみると暴走することがある。これは配列の容量を超えて処理を 行うと、他の変数領域や、プログラムが格納されているメモリー、OSの領域を破壊 してしまうためである。本ホームページでは、配列の容量のチェックをするルーチンは 省いているが、実際にプログラムを作る際には、必ず容量を超えないように 注意しなければならない。

本ホームページのように勉強のためにプログラムを作って、自分だけが実行する 分には、使うときに容量を超えないように自分が注意すれば良いが、実際に他人が使う ときは、その人がどんな操作をしても暴走したりし ないように、プログラムを作らなければならない。これは 文字列処理に限ったことではなく、プログラムを作る時には、すべてに当てはまる注意である。


7章:文字列

7-3.文字列のコピー

例題)
文字列Aを配列Bにコピーするサブルーチンを作る。
残念ながら、C言語では配列のコピーを「B=A;」とは書けない。そこで、１文字ずつ コピーしなくてはならず、NULLがあったらそれをコピーして終わる。
文字列A | 　'M' | 'O' | 'J' | 'I' | 'R' | 'E' | 'T' | 'S' | 'U' | NULL
  ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
↓
文字列B | 　'M' | 'O' | 'J' | 'I' | 'R' | 'E' | 'T' | 'S' | 'U' | NULL

#include <stdio.h>
void s_copy();

char A[21],B[21];
int i;

void main(){

    printf("文字列を入力してね  (20文字以内)\n";
    scanf("%20s",A);

    s_copy();

    printf("　入力された文字列 : %s \n",A);
    printf("コピーされた文字列 : %s \n",B);
}

void s_copy(){

    /* ここを考えてね */
}


7章:文字列

例題)
文字列Aを配列Bにコピーするサブルーチンを作る。
残念ながら、C言語では配列のコピーを「B=A;」とは書けない。そこで、１文字ずつ コピーしなくてはならず、NULLがあったらそれをコピーして終わる。
文字列A | 　'M' | 'O' | 'J' | 'I' | 'R' | 'E' | 'T' | 'S' | 'U' | NULL
  ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
↓
文字列B | 　'M' | 'O' | 'J' | 'I' | 'R' | 'E' | 'T' | 'S' | 'U' | NULL

#include <stdio.h>
void s_copy();

char A[21],B[21];
int i;

void main(){

    printf("文字列を入力してね  (20文字以内)\n";
    scanf("%20s",A);

    s_copy();

    printf("　入力された文字列 : %s \n",A);
    printf("コピーされた文字列 : %s \n",B);
}

void s_copy(){

    i=-1;

    do{
        i++;
        B[i]=A[i];

    }while (A[i] != 0);
}


7章:文字列

7-4.練習問題

  問題
文字列Aのm番目からn個分の文字列を配列Bにコピーするサブルーチンを作れ。 ただし、m,nの値が文字列Aの範囲を超えるような値が入力されたらerr_flg=1 にし、正常にコピーできたらerr_flg=0を返す。

    1
2
3
4
5
6
7
8
9
文字列A 　'M' 'O'
'J'
'I' 'R' 'E' 'T' 'S' 'U' NULL 　　m=4, n=2
文字列B 　'I' 'R' NULL
    1
2
3
4
5
6
7
8
9
文字列A 　'M' 'O'
'J'
'I' 'R' 'E' 'T' 'S' 'U' NULL 　　m=7, n=4
エラー
    1
2
3
4
5
6
7
8
9
文字列A 　'M' 'O'
'J'
'I' 'R' 'E' 'T' 'S' 'U' NULL 　　m=10, n=1
エラー


7章:文字列

7-4.練習問題

  問題
文字列Aのm番目からn個分の文字列を配列Bにコピーするサブルーチンを作れ。 ただし、m,nの値が文字列Aの範囲を超えるような値が入力されたらerr_flg=1 にし、正常にコピーできたらerr_flg=0を返す。

    1
2
3
4
5
6
7
8
9
文字列A 　'M' 'O'
'J'
'I' 'R' 'E' 'T' 'S' 'U' NULL 　　m=4, n=2
文字列B 　'I' 'R' NULL
    1
2
3
4
5
6
7
8
9
文字列A 　'M' 'O'
'J'
'I' 'R' 'E' 'T' 'S' 'U' NULL 　　m=7, n=4
エラー
    1
2
3
4
5
6
7
8
9
文字列A 　'M' 'O'
'J'
'I' 'R' 'E' 'T' 'S' 'U' NULL 　　m=10, n=1
エラー

#include <stdio.h>

void blc_copy();

void main(){
    int m,n,i,flag;
    char A[51];

    printf("文字列を入力してください(50文字以内)\n";
    scanf("%50s",A);
    printf("開始位置は? => ");
    scanf("%2d",m);
    printf("文字数は? => ");
    scanf("%2d",n);

    blc_copy();

    if (flag==0)
        printf("%s\n",B);
    else
        printf("エラーです\n");
}

void blc_copy(){

    /* ここを入力してね */

}


7章:文字列

7-4.練習問題

  問題
文字列Aのm番目からn個分の文字列を配列Bにコピーするサブルーチンを作れ。 ただし、m,nの値が文字列Aの範囲を超えるような値が入力されたらerr_flg=1 にし、正常にコピーできたらerr_flg=0を返す。

    1
2
3
4
5
6
7
8
9
文字列A 　'M' 'O'
'J'
'I' 'R' 'E' 'T' 'S' 'U' NULL 　　m=4, n=2
文字列B 　'I' 'R' NULL
    1
2
3
4
5
6
7
8
9
文字列A 　'M' 'O'
'J'
'I' 'R' 'E' 'T' 'S' 'U' NULL 　　m=7, n=4
エラー
    1
2
3
4
5
6
7
8
9
文字列A 　'M' 'O'
'J'
'I' 'R' 'E' 'T' 'S' 'U' NULL 　　m=10, n=1
エラー

#include <stdio.h>

void blc_copy();

void main(){
    int m,n,i,flag;
    char A[51];

    printf("文字列を入力してください(50文字以内)\n";
    scanf("%50s",A);
    printf("開始位置は? => ");
    scanf("%2d",m);
    printf("文字数は? => ");
    scanf("%2d",n);

    blc_copy();

    if (flag==0)
        printf("%s\n",B);
    else
        printf("エラーです\n");
}

void blc_copy(){
    i=0;
    flag=0;
    while(A[i]!=0)
        i++;

    if((i>m+n-1) && (m>=0) && (n>=0) && (i!=m)){
        i=0;
        while(i<n){
            B[i]=A[m+i];
            i++;
        }
    }
    else{
        i=0;
        flag=1;
    }
    B[i]=0;
}


7章:文字列

7-5.文字列の比較

    文字列の比較とは、文字列の大小関係を求めることである。イメージ的には、 英和辞典の前の方にある単語は小さく、最後の方にあるものは大きいという。 すなわち、「computer」と「compare」では 「compare」の方が前のページにある。したがって、「compare」 の方が小さく、「computer」の方が大きいという。

    また、「run」と「runaway」では「runaway」 の方が後の方に載っているので、「runaway」の方が大きいと 言うことになる。

    英和辞典は基本的に小文字で掲載されているが、文字列の比較では、大文字、 小文字は基本的には区別される。これは文字コードで比較するからであり、記号や 数字が混じっている場合もすべて、文字コードで比較する。

例題
２つの文字列A、Bを比較し、Aの方が大きい場合は変数re=+1を返し、 Aの方が小さい場合はre=-1を、同じ場合はre=0を 返すサブルーチン「s_comp」を作れ。


7章:文字列

例題
２つの文字列A、Bを比較し、Aの方が大きい場合は変数re=+1を返し、 Aの方が小さい場合はre=-1を、同じ場合はre=0を 返すサブルーチン「s_comp」を作れ。

#include <stdio.h>
void s_comp();

    int i,re;
    char A[30],B[30];

void main(){
    printf("文字列A =>> ");
    scanf("%29s",A);
    printf("文字列B =>> ");
    scanf("%29s",B);

    s_comp();
    switch (re){
        case -1 : printf("文字列Aは小さいです\n"); break;
        case  1 : printf("文字列Aは大きいです\n"); break;
        default : printf("同じ文字列です\n"); break;
    }
}

s_comp(){
    re=0;
    i=-1;

    do{
        i++;
        if (A[i]!=B[i]){
            if(A[i]>B[i])
                re=1;
            else
                re=-1;
        }
    }while(A[i]!=0 && re==0);
}

別解：
#include <stdio.h>
void s_comp();

    int i,re;
    char A[30],B[30];

void main(){
    printf("文字列A =>> ");
    scanf("%29s",A);
    printf("文字列B =>> ");
    scanf("%29s",B);

    s_comp();
    switch (re){
        case -1 : printf("文字列Aは小さいです\n"); break;
        case  1 : printf("文字列Aは大きいです\n"); break;
        default : printf("同じ文字列です\n"); break;
    }
}

s_comp(){
    i=-1;

    while(A[i]!=B[i] && A[i]!=0){
        i++;

        if (A[i]!=B[i]){
            if(A[i]>B[i])
                re=1;
            else
                re=-1;
        }
        else
            re=0;
    }
}


7章:文字列

7-6.文字列の連結

読んで字のごとく、文字列をつなげる。
文字列A　：　'I'　'　'　'l'　'o'　'v'　'e'　'　'　NULL
文字列B　：　'S'　'P'　'E' 'E'　'D'　NULL
文字列C　：　'I'　'　'　'l'　'o'　'v'　'e'　'　'　'S'　'P'　'E' 'E'　'D'　NULL

例題
文字列Aと文字列Bを連結させて、文字列Cを作るサブルーチン「s_cat」を作れ。


7章:文字列

例題
文字列Aと文字列Bを連結させて、文字列Cを作るサブルーチン「s_cat」を作れ。
#include<stdio.h>
void s_cat();
    int i,j;
    char A[30],B[30],C[60];

void main(){
    printf("文字列A =>> ");
    scanf("%29s",A);
    printf("文字列B =>> ");
    scanf("%29s",B);

    s_cat();

    printf("文字列C =>> %s\n",C);
}

s_cat(){
    i=0;
    j=0;

    while(A[i]!=0){
        C[i]=A[i];
        i++;
    }

    do{
        C[i]=B[j];
        i++;
        j++;

    }while(B[j+1]!=0);
}
※scanf()は半角スペースを「区切り」として認識するために入力することはできません


7章:文字列

7-7.文字列の検索

読んで字のごとく、文字列を検索する。
文字列A　：　"This_is_a_pen.",NULL
文字列B　：　"is",NULL
結果表示 :   ２個です。

文字列A　：　"This_is_a_pen.",NULL
文字列B　：　"isa",NULL
結果表示 :   ありません

文字列A　：　"1111",NULL
文字列B　：　"11",NULL
結果表示 :   ２個です。

  これは2個と数える人と3個と数える人がいると思いますが、
  ここでは2個と数えることにします。

これはいくつかの考え方がありますが、ここでは以下のように考えます。
  1. 文字列Bの先頭の1文字目を文字列Aから探します。
  2. 文字列Bと文字列Aが異なるまで(同じ間は)比較します。
  3. 異なったところの文字列BがNULLを指していたら、すべて一致していると判断し、カウンタを1増やします。
  4. 以上のことを文字列AがNULLになるまで繰り返します。

例題
文字列Aから文字列Bを検索し、変数compareに個数を返すサブルーチン「s_search」を作れ。


7章:文字列

例題
文字列Aから文字列Bを検索し、変数compareに個数を返すサブルーチン「s_search」を作れ。

#include <stdio.h>

    char A1[71],A2[71];
    char i,j;
    char cnt;
/*	char stock;*/

void main(){

    printf("\n文字列1 >> ");
    scanf("%70s",moji1);
    printf("文字列2 >> ");
    scanf("%70s",moji2);

    cnt=0;
    i=0;
    while (A1[i]!='\0'){
        while(A[i]!=0 && A1[i]!=A2[0])
            i++;
/*		stock=i;*/
        j=0;

        while (A1[i]!='\0' && A1[i]==A2[j]){
            i++;
            j++;
        }

        if  (A2[j]=='\0')
            n++;
/*		i=stock;
        i++;*/
    }

    if (cnt==0)
        printf("ないよ\n");
    else
        printf("%d 個みっけた\n",cnt);
}

緑のコメント文を実行すると「文字列A　:　1111」「文字列B　:　11」 を入力した際に「3」と出力するが、これをやるなら、アルゴリズムを見直し た方が、効率の良いプログラムになる。
← 前ページへ　:　トップへ　:　終了ですお疲れさまでした


アルゴリズム入門

    アルゴリズム入門へようこそ。

    アルゴリズムはすべてのプログラムの基礎となる物です。バグのないプログラムを作るため、 また、再利用性の高いプログラムを作るため、さらにはコンピュータの思考を理解するために 必要な知識です。プログラム言語を学ぶ前にぜひアルゴリズムをきわめて下さい。

    また、私がよく読むアルゴリズムの本を紹介します。今手に入るかどうかはわかりませんが。。。

出版社 | タイトル | ISBN | 内容
技術評論社 | C言語による最新アルゴリズム事典 | 4-87408-414-1 | アルゴリズム集
技術評論社 Javaによる はじめてのアルゴリズム入門 4-7741-1240-2 色々なアルゴリズムが解説されています。
このサイトを読んだらぜひどうぞ
日本実業出版社 | 図解でわかるアルゴリズム入門 | 4-534-03010-X | 初心者向け
本の検索・ご購入は「 コンピュータの本・専門店」で。

←目次から選んでください

・ トップページに戻る


アルゴリズム入門

トップ->アルゴリズム入門

あなたは

人目のアルゴリズム入門受講生です。

目次
0. はじめに

1. アルゴリズム
      1. アルゴリズムとは
      2. 当ページの範囲
      3. 良いアルゴリズム
      4. フローチャートの書き方(記号)
      5. フローチャートの書き方(構造)
      6. 順次構造
      7. 分岐構造
      8. 反復構造

2. 順次構造
      1. 代入
      2. 計算
      3. 入力
      4. 出力
      5. 練習問題1
      6. 練習問題2

3. 分岐構造
      1. 条件分岐
      2. 単一分岐
      3. 練習問題1
      4. 多重分岐
      5. 複合条件
      6. ケース(多方向分岐)
      7. 練習問題2
      8. 練習問題3

4. 反復構造
      1. 反復構造の種類
      2. 前判定型
      3. 後判定型
      4. 練習問題1
      5. 練習問題2
      6. 練習問題3
      7. 多重反復処理(ネスト)
      8. 練習問題4
      9. 練習問題5

5. 配列
      1. 配列とは
      2. 練習問題
      3. ２次配列
      4. 練習問題
      5. 素数を求める,その1
      6. 素数を求める,その2

6. 検索・ソート
      1. サブルーチン
      2. シーケンシャルサーチ
      3. バイナリサーチ
      4. 練習問題
      5. ルックアップテーブル
      6. ソートとは
      7. 選択法
      8. 交換法
      9. バブルソート
      10. 挿入法
      11. クイックソート
      12. 処理速度の比較

7. 文字列
      1. 文字と文字列
      2. 文字列処理
      3. 文字列のコピー
      4. 練習問題
      5. 文字列の比較
      6. 文字列の連結
      7. 文字列の検索

・ トップページに戻る


はじめに

    巷では完全にMS-Windowsの天下ですね。AppleもiMacでがんばってはいますが。そんな 世の中にアセンブリ言語を学ぼうという人はほとんどいないでしょうね。よく人から 「アセンブラなんて使うの？」なんてことを言われます。答えは「Yes」 であり、「No」でもあります。

    趣味でプログラミングをする人でアセンブラをやる人は、現在はほとんどいないでしょう。 世の中のパソコンが進歩したからです。昔のパソコンはメモリーも少なく、処理も遅かった ので、何かの処理を速くやりたい場合や、ドライバーなどの常駐プログラムを作る場合は アセンブラが主流だったのです。

    しかし、技術の進歩は速いもので、メモリーは膨大に増え、処理速度も速くなってきたので、 そんなことを考えてプログラムを作るより、機能をあげたほうがいいと考えられているからです。 では、アセンブラは最近は必要なくなったのでしょうか？　今回の質問の答えは「 No!」です。

    アセンブラとはコンピュータの命令１つ(2進数の数字)を、人間が理解しやすくしたもので、 アセンブラの命令１つがマシン語の１つに対応しています。C言語などの高級言語は、 高級言語の命令１つは、マシン語の命令が複数集まってできています。例えば、C言語 でディスプレイに文字を表示するという命令や、文字を入力する命令は、誰かがマシン語で あらかじめ作ってくれて、C言語のプログラマーは、それらを組み合わせているだけなのです。

    ディスプレイやキーボードは古くからあるデバイス(装置)なので、これを操作する マシン語のプログラムはC言語のコンパイラーを買った時に標準でついてきます。しかし、 新しいデバイス(装置)を作った場合、これを操作するプログラムはC言語にはないので、 誰かが作らなければなりません。何で作るのでしょう？　C言語には用意されていないので、 C言語では作れません。もちろん他の高級言語にも用意されていないので、アセンブラで 書くしかないのです。したがって、趣味で使う人は少なくなっても、デバイスメーカーは アセンブラなくしては、製品の開発ができないのです。

    もう少しわかりやすい例を出します。IntelはPentiumというCPUを作りました。 その次にMMX-Pentiumというのを作りました。MMXとは「Multi　Media　 Extention　Technology」の略で、マルチメディアを扱うのに便利な新しい 命令を数百増やしたCPUです。CPU自体の命令を増やしても使わなくては仕方がありません。 C言語でそのMMX命令を使うためには、アセンブラでC言語用の命令を作らなければなりません。 このような観点からアセンブラは無くてはならない言語です。

    また、パソコンが進化してきたと言っても、まだまだ小さいコンピュータは使われています。 例えば、洗濯機や冷蔵庫、炊飯器にカメラさらにはテレビやそのリモコンまで。 電気や電池を必要とするもののほとんどにコンピュータは使われています。 しかし、それらは値段が高くては誰も買ってくれないので、必要最小限のコンピュータしか 使いません。そうするとPentiumなんて使うわけにはいきません。せいぜい4bitCPUとかです。 当然、メモリーも必要最小限です。少ないメモリー、遅いCPUでプログラムを書くのに、 高級言語なんて使えません。やはりアセンブラの登場と言うことになります。

    このように趣味でアセンブラをやる人は少なくなりましたが、この世の中には 必要な言語です。

    ここで、アセンブラの特徴(?)を紹介します。
  • コンピュータの仕組みを学ぶにはアセンブラが1番
  • 高級言語は裏で何しているかわからん!!
  • 高級言語はバカな処理をすることがある!!
  • CPUが変わると命令も変わるが、基本は一緒
  • パズル好きな人向け
  • 「この世で信用できるのは自分だけ!!」という人向け
← 前ページへ　:　トップへ　:　次ページへ →


レジスタとは

基礎

1-1.レジスタとは

    一般にメモリーと呼んでいるメモリーは正確には「主記憶装置」と言います。 この他に、CPU(演算装置)内にも少ないけれどもメモリーがあります。このメモリーは 安くて低速な主記憶装置とは異なり、速くて高いメモリーです。このメモリーには、 次に実行する命令、次に実行する命令がどこにあるのかを示すアドレス、 主記憶装置にアクセスするためのセグメント(リアルモード)もしくはセレクタ (プロテクトモード)、さらにレジスタと呼ばれる変数みたいなものなどが 記憶されています。言い換えればそれしか記憶できない小さなメモリーです。 このホームページでは「メモリー」は主記憶装置を指し、、「レジスタ」は CPU内のメモリー(レジスタ)を指すことにする。

    最近のCPUでは、CPUと主記憶装置の間にキャッシュと呼ばれる高速で高価な メモリーを持っています。この高速なメモリーは低速な主記憶装置に比べて、 約10倍の速度でアクセスできます。

    さて、このページでは基本的に16bitのアセンブラについて書いていますので、 最近のCPUのことはこのくらいにします。

    このCPU内の小さなメモリーのことをもう少し詳しく説明します。話ばかりで、 退屈かと思いますが、アセンブラにとっては大事なことなので、我慢してください。

    このメモリーのうち、実行すべき命令を覚えるメモリー以外をレジスタと呼びます。 レジスタには以下のような種類があり、汎用レジスタは今は変数だと思っていてください。

    8086,80186,(V30),80286では緑の部分しか使えません。

    AH, AL, BH, BL, CH, CL, DH, DLレジスタは8bitすなわち1byteの大きさです。

    AX, BX, CX, DX, SI, DI, BP, SP, IP, FLAGS, CS, SS, DS, ES, FS, GSは16bit(2bytes)の大きさです。

    EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, EIP, EFLAGSは32bit(4bytes)の大きさです。

    AXはAHとALから構成されており、AHやALを変更するとそれに伴ってAXやEAXも変化しますし、AXを 変更するとEAXやAHもしくはALも変化します。以下、BX, CX, DXも同様です。

    さらにSI, DI, BP, SP, IP, FLAGSを変更するとESI, EDI, EBP, ESP, EIP, EFLAGSも変化します。
    次に特別なレジスタについて説明します。

CS
次に実行すべき命令があるアドレスのセグメントを指します。

SS
スタックを格納するアドレスのセグメントを指します。

DS,ES
データの読み書きをするアドレスのセグメントを指します。

EIP(IP)
次に実行すべきアドレスのオフセットを指します。

EFLAGS(FLAGS)
CPUの状態や前の命令の実行結果のエラー状態が入ります。 詳しくは後に譲ります。

ESP(SP)
スタックを格納するアドレスのオフセットを指します。

EBP(BP)
ここでは「特別な用途に使用し、滅多に変更しない」と覚えておいてください。

ESI,EDI(SI,DI)
ストリング命令に使用しますが、何に用いても良い。

ECX(CX)
何に用いても良いが、主にカウンタとして使うことが多い。

EAX,EBX,EDX(AX,BX,CX)
何に用いても良い。


レジスタとは

1-2.セグメントとは

    8086,80186,(V30),80286のアドレスバスは20bitです。アドレスバスとは、電話番号の 桁数のようなものです。10桁の電話番号では10,000,000,000個の電話を管理できることに なります。20bitだと16進数で5桁すなわち1Mバイト分のアドレスを管理できることになります。 しかし、データバスやレジスタは16bitであり、16進数で4桁しか覚えられません。電話番号を メモしなければならないのに、メモ用紙に8桁しか書けなければ電話番号をメモすることは できません。そこでメモ用紙を2枚使用することになります。しかし2枚用いると、残りの 8桁分メモできるのにもったいないです。

    そこで例えば、03-3123-4567をメモする場合に、1枚目に03-3123-45とメモし、2枚目に 0000-0067とメモし、これを2桁ずらして足します。そうすると03-3123-4567となります。 03-0000-00、3123-4567とメモしても同じです。「0」で埋めなくても、03-1111-11と 2012-3467としても同じです。

0 3 - 3 1 2 3 - 4 5
+) 0 0 0 0 - 0 0 6 7
   0 3 - 3 1 2 3 - 4 5 6 7
0 3 - 0 0 0 0 - 0 0
+) 3 1 2 3 - 4 5 6 7
   0 3 - 3 1 2 3 - 4 5 6 7
0 3 - 1 1 1 1 - 1 1
+) 2 0 1 2 - 3 4 6 7
   0 3 - 3 1 2 3 - 4 5 6 7

    これをアドレスについても適用させます。12345hというアドレスを16bitレジスタ 2つで表すのに、1234hと0005hと記憶し、これを1桁ずらして足すことで、12345hと表すことが できます。これを「1234h:0005h」と表記します。「1000h:2345h」や「1111h:1235h」 も12345hを表します。

1 | 2 | 3 | 4
+) 0 0 0 5
   1 | 2 | 3 | 4 | 5
1 | 0 | 0 | 0 | 0
+) 2 3 4 5
   1 | 2 | 3 | 4 | 5
1 | 1 | 1 | 1
+) 1 2 3 5
   1 | 2 | 3 | 4 | 5

    1234h:0005hと表したとき、上の4桁「1234h」をセグメント、下の4桁「0005h」を オフセットと呼びます。


MOV命令

2-1.MOV命令

    MOV命令とは、一番基本的で、一番頻繁に使われる命令である。どんなことをするかというと データの複写である。機能は「複写」と言うが、「転写する」もしくは「転送する」という。 すなわち、レジスタからメモリーに、メモリーからレジスタに、ある数字をレジスタもしくは メモリーにコピーする事を言う。ただし、メモリーからメモリーへの転写はできない。これは、 アドレスバスが20bit(32bitCPUは32bit)であり、一度にアクセスできるメモリーの アドレスを１つ分しか指定できないからである。

    MOV命令は２つの引数を指定して使う。この引数のことをアセンブラでは「オペランド」 と呼ぶ。実際には以下のような感じで使用する。

MOV	DEST,SRC
    動作：DEST←SRC
    影響を受けるフラグ：なし
    DEST：レジスタ、メモリー
    SRC ：レジスタ、メモリー、即値（ただしメモリー、メモリーの組み合わせは除く）
     「フラグ」については後で述べる。

    MOV命令で転送できるのは、バイト(8bit)、ワード(2bytes)、ダブルワード(4bytes,32bitCPUのみ) である。
MOV AX,BX　・・・　BXレジスタの内容をAXに代入せよ
MOV AL,10　・・・　ALレジスタに10を代入せよ
    ここで、以下の点に注意しなければならない。
  • メモリー間の転送はダメ
  • 転送元と転送先の大きさは同じ容量の入れ物でなければならない
  • セグメントレジスタ間の転写、セグメントレジスタへ即値の転写はできない
    では次ページから実際の使い方や、注意事項を説明していく。


MOV命令

例)　AHレジスタに「2」を代入する

        MOV AH,2

例)　DLレジスタにALレジスタの内容をコピーする

        MOV CL,AL


MOV命令

2-2.コンパイル

    まだMOV命令しかやっていないので、プログラムを作ることもできません。なぜなら、 プログラムを終了して、処理をOS(このホームページではMS-DOSを前提としている) に処理を戻すという処理もできないからです。しかし、実際にプログラムを実行 しなければつまらないので、解説が出てくるまで丸暗記してほしい部分が結構あるが ご了承ください。

    ところで、MS-DOSの実行形式のファイルは３種類あります。拡張子が「.BAT」 、「.COM」そして「.EXE」です。「.BAT」は バッチファイルといって、実行したいプログラムをどのような順番で実行するかと言った テキスト形式のもので、プログラムとはちょっと違います。詳しくはMS-DOSの本やマニュアルを 参照してください。「.COM」ファイルは、マシン語の羅列です。 扱うセグメントレジスタはすべて同じ値を示し、１つのセグメント領域しか 使わないのです。つまりFFFFh(64Kbytes)を越えるプログラムは書けません。初心者には これが一番わかりやすいので、本ホームページはすべてこの形式で書いてあります。 「.EXE」ファイルは、複数のセグメントにわたってプログラムが書けます。 一般には、プログラム本体が書いてあるセグメント、データを格納するセグメントと 言った風に、用途によってセグメントを使い分けています。

    では、下記のようなプログラムを書いてください。
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	ah,2
    mov	dl,'A'
    int	21h

    mov	ax,4c00h
    int	21h

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

    青いところがプログラムです。黒いところは「.COM」ファイルを作るという おまじないです。本ホームページでは「.COM」しか扱わないので、 以後この部分を省略しますが、プログラムを実際に作るときにはこの部分も 入力してください。

    まず１行目で、１つ目のセグメントの名前を「CODE」にすると 定義しています。そして２行目で、DS,CS,ES,SSレジスタはこのCODEセグメントを 指すようにしています。つまり１つのセグメントしか使わず、「.COM」 ファイルということになります。

    ３行目は80186以上のCPUを前提としていることをコンパイラに伝えています。

    「.COM」ファイルでは、実際のプログラムが始まるのは、プログラムが ロードされたセグメントの100h番地から始まるので、４行目でそのことを定義しています。

    そして、最後の２行で、プログラムの終わりを宣言しています。プログラムの終わりというのは、 MS-DOSに戻るという意味ではなく、コンパイラにここまでがプログラムだよと伝えるだけです。

    では次にプログラムの解説をします。

mov ah,2
前のページで解説したMOV命令です。AHレジスタに2を代入します。

mov dl,'A'
文字をシングルクォーテーションで囲うと、その文字コードを返します。すなわち、 'A'の文字コード41hをDLレジスタに代入しています。

int 21h
システムコールと呼ばれるもので、AH=2の状態でINT　21hを実行すると、 DLに入っている文字１文字ディスプレイに表示します。後で解説しますので、 ここではそういうものだと覚えておいてください。

mov ax,4c00h
AXレジスタに4C00hを代入しています。

int 21h
これもシステムコールです。AXに4C00hが入っていると言うことは、AHに4Ch、ALに0が 入っています。AHに4Chが入っているときにINT　21hを実行すると、プログラムが 終了して、MS-DOSに処理が移ります。これも覚えておいてください。

    さて、これを実行形式「.COM」ファイルにしなければなりません。 エディターで上のプログラムを書いたら、「TEST.ASM」というファイル名で 保存してください。(別にファイル名は好きなものでよいが、拡張子は「.ASM」)

ここではBorlandaのTurbo Assemblerを例にします。
  1. TASM　TEST.ASM
  2. TLINK　/t　TEST.OBJ
の順で、実行します。最初の「TASM」で、エラーが出る場合には、 どこか間違ってプログラムを書いていることになります。ここでエラーにならなくても、 いざ実行するとエラーになったり、暴走したりすることもあるので、気をつけてください。

では「TEST.COM」を実行してみてください。「A」と表示されたら成功です。


MOV命令

2-3.即値の転送

    即値の転送とは定数をレジスタもしくはメモリーに転送することを言う。別に、 「即値の転送」なんて名前はどうでも良いんですが・・・。ちなみに、イミディエイト モードという場合もあるらしいです。名前なんてどうでも良いんですが

    基本的なことは前のページまでで解説しました。しかし数字はその時々によって、 10進数、16進数、8進数、2進数、文字コード、メモリーの番地などを代入したい場合が あります。C言語と同様、アセンブラでもこれらを表現することができます。
mov ah,10
AHレジスタに、10進数で10を代入します
C言語表記　：　ah=10;

mov ah,10h
AHレジスタに、16進数で10(10進数で16)を代入します
C言語表記　：　ah=0x10;

mov ah,10o
AHレジスタに、8進数で10(10進数で8)を代入します

mov ah,10b
AHレジスタに、2進数で10(10進数で2)を代入します

mov ah,'A'
AHレジスタに、'A'の文字コード(10進数で65、16進数で41h)を代入します
C言語表記　：　ah='A';

mov ax,offset DATA
AXレジスタに、DATAという変数のアドレスを代入します
C言語表記　：　ax=&DATA

「mov ax,'A'」　というふうに書くとどうなるでしょうか？
「mov ax,41h」と同じことなので、AH=00、AL=41hが入ります。


MOV命令

2-4.メモリーへの転送

    メモリーへの転送(メモリーからの転送)は２種類の方法がある。「直接アドレス法」と 「間接アドレス法」である。

2-4-1.直接アドレス法

    直接アドレス法とは、メモリーのアドレスを指定して転送する方法である。 例えば、「DATA」という変数にALレジスタの内容を転送したい場合、下記のように書く。
mov DATA,al
    また、決まったアドレスに書き込みたい場合は、下記のように[・・・]で囲う。
mov [1000h],al

    メモリーに即値を代入したい場合はちょっと面倒になる。例えば、変数DATAに、 0を代入したい場合、その0が「00h」(バイト)か「0000h」(ワード)か「00000000h」 (ダブルワード)かアセンブラにはわからないからである。レジスタの場合は、 レジスタの大きさがわかっているために、このようなことは起こらない。しかし、 メモリー←即値の場合はどの単位なのか指定してやらなければならない。 その場合、下記のように「byte ptr」「word ptr」などと指定する。ただし コンパイラの種類によってはどこに「byte ptr」を書くのかに差がある。 詳しくはお手持ちのコンパイラのマニュアルを参照してください。
mov byte ptr DATA,0
mov word ptr DATA,0
      or
mov DATA,byte ptr 0
mov DATA,word ptr 0

2-4-2.間接アドレス法

    間接アドレス法とはメモリーのアドレスをレジスタを使って指定する方法を言う。 C言語で言う「ポインタ」である。
mov bx,offset DATA
mov byte ptr [bx],0
      or
mov [bx],byte ptr 0
      or
mov [byte ptr bx],0
    ただし使用できるレジスタは下記のもの(80286以下のCPU)しかも値いることができない。
[BX]、[SI]、[DI」
[BX+SI]、[BX+DI]、[BP+SI]、[BP+DI]
[BX+disp]、[SI+disp]、[DI+disp」
[BX+SI+disp]、[BX+DI+disp]、[BP+SI+disp]、[BP+DI+disp]

    ※disp : 即値

2-4-3.ワード以上の単位の場合の注意

    ワード以上の単位でメモリーに格納する場合、ちょっとした注意が必要になる。 「mov　[1000h],word ptr 1234h」とした場合、1000h番地には「34h」が入り、 1001h番地には「12h」が入る。これはx86系CPUの仕様で、他のCPUでは逆になる場合もある。

    また、ダブルワードの場合も同様に逆順に入るため、 「mov　[1000h],dword ptr 12345678h」とした場合、1000h番地から 78h,56h,34h,12hと入る。


MOV命令

例）
先ほどの「A」と表示するプログラムを直接アドレス法、間接アドレス法を用いて 書き換えると次のようになる。

直接アドレス法
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	ah,2
    mov	dl,DATA
    int	21h

    mov	ax,4c00h
    int	21h

DATA	byte	'A'

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

間接アドレス法
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	ah,2
    mov	bx,offset DATA
    mov	dl,[bx]
    int	21h

    mov	ax,4c00h
    int	21h

DATA	byte	'A'

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

    「DATA byte 'A'」は、変数「DATA」をバイト型で作り、 初期値として'A'の文字コードを入れておけという命令です。

    ラベル名で使用できる文字は「アルファベット」「数字」「一部の記号」です。 数字以外の文字で始まり、247文字以内でなければなりません。また予約語、 アセンブラの命令は当然使えません。大文字小文字の区別は普通しないが、 コンパイラにもよります。記号は特別な用途に用いられるものもあるため、 「_(アンダーバー)」以外はあまり使いません。


MOV命令

2-5.練習問題

    DATA1の内容(ABCD)をDATA2にコピーして、DATA2を表示するプログラムを作りたい。 下記の空欄を埋めよ。(ただし、DATA1とDATA2の後ろにある24hは気にしなくても良い)
START:
                           ;
                           ; DATA1の内容を
                           ; DATA2にコピーする
                           ;

    mov	ah,9               ;
    mov	dx,offset DATA2    ; DATA2を表示する
    int	21h                ;

    mov	ax,4c00h
    int	21h                ; 終了

DATA1	db	'ABCD',24h
DATA2	db	'0000',24h
AH=9のときINT　21hを実行するとDXのアドレスから24h('$')があるまで文字列を表示する。


MOV命令

2-5.練習問題

    DATA1の内容(ABCD)をDATA2にコピーして、DATA2を表示するプログラムを作りたい。 下記の空欄を埋めよ。(ただし、DATA1とDATA2の後ろにある24hは気にしなくても良い)
START:
    mov	si,offset DATA1
    mov	di,offset DATA2
    mov	al,[si]
    mov	[di],al

    mov	al,[si+1]
    mov	[di+1],al

    mov	al,[si+2]
    mov	[di+2],al

    mov	al,[si+3]
    mov	[di+3],al

    mov	ah,9               ;
    mov	dx,offset DATA2    ; DATA2を表示する
    int	21h                ;

    mov	ax,4c00h
    int	21h                ; 終了

DATA1	db	'ABCD',24h
DATA2	db	'0000',24h
AH=9のときINT　21hを実行するとDXのアドレスから24h('$')があるまで文字列を表示する。


算術命令

算術演算

3-1.符号なし演算と符号付き演算

    コンピュータが理解できる数字は2進数であり、これは人間には理解しにくい。 そこで人間が理解できるように変換する必要がある。この変換の方式は用途、 精度、変換のしやすさなどによりいくつかに分けられる。高級言語では そのうち汎用性の高い方法を採用しているが、アセンブリ言語では計算の 仕方を変えるだけで、すべての方法を用いることができる。

    高級言語では扱える種類は限られるが、手続きをすれば簡単に且つ、 ミスの無いように扱うことができる。これに反しアセンブリ言語では、 すべての種類を扱える反面、「この数字はこの方法で扱う」というのは プログラマーが管理しなくてはならず、ミスしやすい。

    このページでは数の表現方法を紹介し、アセンブリ言語で頻繁に使われる 「符号付き整数」と「符号なし整数」について解説する。

    数の表現は目的などによっていくつかの方法があることは上で述べた。 その種類は下記の通りである。

2進化10進法(BCD)
10進数の各桁をそれぞれ4桁(4bit)の2進数で表したもの。
例）10進数の193をBCDで表すと
1
9
3
↓
↓
↓
0001 0101 0011
したがって(193)10=(000101010011)2となる。
2進数の並びのうち、1010～1111は使わないので、効率は悪い。

10進ゾーン形式
BCDの1種で、アンパックBCDとも呼ぶ。10進数の1桁を8bit(1バイト)の2進数で表す。 10進数の1桁は2進数の4桁(4bit)で表すことができるので、残りの4桁(4bit)を何かの 数字で埋めなくてはならない。通常10進数の1桁を表す4bitは8bitのうちの下位に 置き、上位4bitは1111(EBCDIC規格)または0011(ASCII、JIS規格)を埋める。また 最後の1桁の上位4bitは符号を置く。1100ならば正、1101ならば負を表す。この 符号部分と1111または0011(上位4bit)をゾーン部、数字を表す下位4bitを 数値部と呼ぶ。

例）10進数の-193を10進ゾーン形式で表すと
1
9
3
↓
↓
↓
JIS | 0011 0001 | 0011 0101 | 1101 0011
EBCDIC | 1111 0001 | 1111 0101 | 1101 0011
したがって(-193)10=(00110001 00110101 11010011)2となる。(JIS規格)

10進パック形式
パック10進数とも言う。10進ゾーン形式ではゾーン部には大して意味のないコードが入るため、 効率が悪い。そこで、10進数の1桁を4bitで表し、最後に符号を付けることで、語長を短くした ものがパック10進数である。

例）10進数の+193を10進ゾーン形式で表すと
1
9
3
+
↓
↓
↓
↓
0001 0101 0011 1100
したがって(+193)10=(0001010100111100)2となる。

固定小数点形式
整数部と小数部の2つに分けて表す方法で、それぞれは符号付き整数と同じである。 あまり見かけない。

浮動小数点形式
極端に大きな数や、極端に小さな数を扱うのに適している。例えば極端に小さい値は 1.2×10-10のように表すことがある。このように指数を用いて表したものが 浮動小数点形式であるが、場合によってはわずかな誤差や桁落ち(絶対値がほぼ等しい 同符号の間での減算もしくは異符号での加算)、情報落ち(非常に大きな数値と小さな数値 との加減算)などが起こる場合もある。
32bitの浮動小数点形式では最上位1bitは符号(0:正、1:負)、次の7bitで指数部( 2の指数+64)、下位24bitは仮数部を表す。

例）10進数の200を浮動小数点形式で表すと
(200)10=(11001000)2=0.11001×28 =0.11001×162
ここで正数であるから符号は「0」、指数は2であるから+64して(66)10=(100010)2 が指数部に、0.11001の「11001」が仮数部に入る。したがって、
(200)10=(0 1000010 110010000000000000000000)2となる。
例）10進数の12.5を浮動小数点形式で表すと
(12.5)10=(1100.1)2=0.11001×24 =0.11001×161
ここで正数であるから符号は「0」、指数は1であるから+64して(65)10=(100001)2 が指数部に、0.11001の「11001」が仮数部に入る。したがって、
(12.5)10=(0 1000001 110010000000000000000000)2となる。

    ここまで真剣に読んでくれた方、ごめんなさい。当ホームページではこれらは使いません。 っていうか、これらの数字を扱う場合はアセンブラよりも高級言語でやった方が数十倍楽です。 次のページで解説する「符号なし整数」「符号付き整数」は大事ですので、ちょっと気分転換でも してください。


算術命令

    では次に整数のお話をします。どんなプログラムでも整数は必ず出てくるので、 整数は小数よりも大事です。ただし小数ほど複雑な構造になっているわけでは ありません。まぁ、当たり前ですが・・・

符号なし整数
    もっとも単純なのが符号なし整数です。例えば8bitならば28の 数が記憶できます。すなわち、0～255(28-1です。16bitならば、 0～65535(216-1)ですし、32bitならば0から232-1までです。 0は00、FF(255)、などそのまんま表現するだけです。

    8bitの場合、255+10など、表現できる値を超えてしまった場合、アセンブラ、 C言語など多くのプログラム言語ではエラーにはなりません。255+10=265で、 8bitでは表現できないので、8bitに丸め込まれてしまいます。すなわち、 255を2進数で表すと(11111111)2で、10は(00001010)2で その結果の265は(100001001)2で、9桁になりますので、9桁以上を 切り捨てて、(00001001)=9になります。16bitの場合は、演算の結果が2進数で17桁 以上になった場合は17桁以上を切り捨てて、16桁に丸め込まれます。32bitも 同様で、32bitに丸め込まれます。このため、どんな言語を使っていても、勝手に 丸め込まれないようにプログラマーが注意しなくてはなりません。

    アセンブラでは、足し算を行った際に、丸め込まれたかを調べることができます。 これについては後で述べます。C言語ではわかりませんので、 必要ならば足し算を行う前に越えるかどうかをチェックしなければなりません。

    引き算の場合も同様なことが起こります。0-1はマイナスになります。 この場合、１つ上の位から桁を借りてきてマイナスします。つまり、8bit の場合、0は100hと解釈され、100h-1=FFhになります。50h-A0hはやはり マイナスになるので、1桁借りてきて、150h-A0h=B0hになります。これについても 後述します。

符号付き整数
    上位1bitを符号として扱います。上位1bitが0の時は正、1の時は負となります。 したがって、8bitの場合、正では0～127、負は-128～-1を表現できます。同様に 16bitでは-32768～+32767までです。正の場合、下位7bitは上述の符号なし整数と 同じです。負の数については「2の補数」という概念で表現されます。

    例えば-5を考えると、+5を2進数で表現すると(00000101)2です。 これの「1の補数」すなわちNOTをとります。（0と1を逆にします。）すると、 (11111010)2です。これに1を加えます。すると(11111011)2 になります。これが「2の補数」の計算の仕方で、(11111011)2=-5と なります。ちなみにこれを符号なし整数で表すとFBhとなります。

    +5が(00000101)2なので、上位1bitの符号を1にして、(10000101)2 が-5になるわけではありません。

    2の補数で負の数を表現すると何が便利なのでしょうか？　まだ演算装置 （いわゆるCPU）が初期の初期のころ、足し算命令はあったけれど、引き算の 命令がないCPUもありました。そこで足し算だけで引き算をする必要がありま した。その結果負の数を2の補数として表すことが考え出されたのです。

    例えば、「15-3」を考えます。これを15と-3を足すと考えます。 15=(00001111)2=0Fhで、-3=(11111101)2=FDhです。 これを足すと、上述したように9桁以上は無視されて0Ch=12となります。 引き算は行わずに引き算をすることができました。

    ところが問題もあります。8bitの場合は-128～+127までですから、 16進数で表現すると-80h～+7Fhです。+7Fh=(01111111)2に 1を加えると、(10000000)2となり、上位1bitが1ですので、 これは-80hになります。正の整数に1を足したら負の数になってしまいました。 これをオーバーフローといいます。これについては後述します。

    すなわち、コンピュータが数値を覚えるというのは、メモリーに記憶する だけのことですから、コンピュータにとっては今覚えた値が、正だろうが 負だろうが、小数だろうがなんだろうが関係ないわけです。プログラマーが どう判断し、コンピュータにどう扱わせるかというだけの話です。高級言語 ではある程度の判断をコンパイラがやってくれます。C言語では、この変数に 入れた値は小数なのか、符号なし整数なのか、符号付き整数なのかなどを 一番最初に宣言しなければなりません。アセンブラではすべてをプログラマが 管理しなければなりません。足し算を行う際も、符号なし整数なのか、符号付き 整数なのかによって、やり方も異なります。

    本ホームページのアセンブラ入門では、符号なし整数しか扱いませんが、 この「アセンブラ入門」に書かれていることを一通り勉強すれば、符号付き 整数の計算の仕方くらいは余裕で考えられると思います。がんばってください。


算術命令

3-2.足し算

    x86系のCPUには足し算１つ見ても命令が２つある。順に見ていくことにする。
ADD	DEST,SRC

    動作：DEST←DEST+SRC
    影響を受けるフラグ：OF,SF,ZF,AF,CF,PF
    DEST：レジスタ、メモリー
    SRC ：レジスタ、メモリー、即値（ただしメモリー、メモリーの組み合わせは除く）
  1. ２つのオペランドを指定し、それらを加算した結果を第１オペランドに格納する。
  2. ２つのオペランドの大きさは同じでなければならない。
  3. MOV命令の時の「直接アドレス法」「間接アドレス法」「即値法」が使える。

例）
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	dl,DATA1
    add	dl,DATA2
    add	dl,'0'

    mov	ah,2
    int	21h

    mov	ax,4c00h
    int	21h

DATA1	byte	5
DATA2	byte	3

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです
「DATA1」すなわち「5」をDLに入れ、「DATA2」すなわち「3」を足して、DLは「8」になる。 そこに'0'の文字コードを足すため、DLは'8'の文字コードになる。そして「INT　21h」で 文字を表示して、終了する。結果、画面には「8」と表示されるプログラムである。


算術命令

ちょっと余談です。呼び飛ばしても何の支障もありません。 呼び飛ばす場合はここをクリック

    前のページでは「5+3=8」をやりました。だからディスプレイには'8'が表示されま した。では「8+5」をやるとどうなるでしょうか？

    「8+5=13=0Dh」です。その後に30hを加えると3Dhになり、これは「=」を意味します。 したがって13とは表示せずに、「=」をディスプレイに表示します。これでは足し算の 意味がないので何とかしたいですね。「ADD」の 後に、「AAA」という命令を実行すると、AH=01、AL=03という風に補正してくれます。
START:
    mov	ah,0            ;AH <=00h
    mov	al,DATA1        ;AL <=08h
    add	al,DATA2        ;AL <=0Dh
    aaa	                ;AH <=01h, AL <=03h

    add	ax,3030h        ;AH <='1'  AL <='3'
    mov	dl,ah           ;DL <='1'
    mov	dh,al           ;DH <='3'
    mov	ah,2
    int	21h             ;'1'表示
    mov	ah,2
    mov	dl,dh           ;DL <='3'
    int	21h             ;'3'表示

    mov	ax,4c00h
    int	21h             ;終了

DATA1	byte	8
DATA2	byte	3
    「AAA」という命令はオペランドをとりません。ALにのみ動作してくれます。


算術命令

    ところで、「AL=90h」の時「ADD　AL,90h」とした場合はどうなるでしょうか？ 90h+90h=120hで、ALレジスタには収まりません。「AX=90h」の時に、「ADD　AX,90h」 とすれば、AX=120hで収まります。収まりきれない場合は、「AL=20h」オーバーフロー した桁は無視されます。

    「ADD」命令ではレジスタの長さ(80286以下のCPUは16bit、80386以上では32bit)しか 計算できないのでは、僕のお小遣いを計算する分には十分ですが、銀行や大企業の場合は 困ります。私たちは日常の計算の中で１桁の足し算は暗算でできますが、桁が多くなると、 筆算をします。
    このように私たちは繰り上がりという 概念を用いて計算しています。
なんとか、この繰り上がりを用いてもっと大きい 数を計算できないか、これを解決するのが、「ADC」命令です。

    実は上の例では、ALレジスタは20hになるが、桁上がりがあるとキャリーフラグという フラグが「１」に変化します。桁上がりがない場合にはキャリーフラグは「０」になります。

    「ADC」は基本的には「ADD」命令と同じであるが、このキャリーフラグが「０」であれば、 DEST←DEST+SRCを行い、キャリーフラグが「１」であれば、さらに１を足します。 つまり、１桁目を足す場合は「ADD」、２桁目より上を足す場合は「ADC」を用いることで、 32bit(80286以下では16bit)よりも桁の多い数の計算が可能となる。
ADC	DEST,SRC

    動作：DEST←DEST+SRC+CF
    影響を受けるフラグ：OF,SF,ZF,AF,CF,PF
    DEST：レジスタ、メモリー
    SRC ：レジスタ、メモリー、即値（ただしメモリー、メモリーの組み合わせは除く）

例）
「ADD　AX,BX」を8bitレジスタの足し算で書き換えると次のようになる。
    mov	ax,DATA1
    mov	bx,DATA2

    add	al,bl
    adc	ah,bh

    mov	ANS,ax


算術命令

トップ-> アセンブラ入門：3章.算術演算-> 3-2.足し算

    さて、符号付き整数の場合はどうなるでしょう。前にも述べましたが、 符号付き整数の場合上位1bitが符号を表しています。したがって8bitの場合、結果が(01111111) 2=7Fhよりも大きくなると、上位1bitが1になり、負の数になってしまいます。 これをオーバーフローといいます。「ADD」、「ADC」のどちらも足し算の結果、オーバーフロー した場合にオーバーフラグというフラグが1になります。足し算の結果オーバーフローしなかった 場合にはオーバーフローフラグは0になります。符号付きの演算ではキャリーフラグは 意味を持ちませんが、オーバーフローフラグが意味を持ってきます。逆に符号なし演算 ではキャリーフラグが「桁あふれ」の意味を持ちましたが、オーバーフローフラグは 意味を持ちません。

    符号なし演算の「ADD」命令で、桁あふれした場合には「ADC」命令を使うことで、 その上の桁を計算することができましたが、符号付き演算でオーバーフローした 場合は、条件分岐をして自分でエラー処理をするしかありません。本ホームページ では符号なし整数しか扱わないので、この処理については割愛しますが、オーバー フローした際の条件分岐については「条件分岐」で説明します。
← 前ページへ　:　 トップへ　:　 練習問題へ →


MOV命令

3-3.練習問題

    下記のDATA1とDATA2の足し算をし、ANSに答えを格納するプログラムを作りたい、 ただし加算は8bitレジスタを用いる。
START:
                       ;
                       ; DATA1とDATA2を足してANSに格納
                       ;
    mov	ax,4c00h
    int	21h

DATA1	dd	0EFFFFFFFh
DATA2	dd	1
ANS	dd	?


MOV命令

3-3.練習問題

    下記のDATA1とDATA2の足し算をし、ANSに答えを格納するプログラムを作りたい、 ただし加算は8bitレジスタを用いる。
START:
    mov	si,offset DATA1
    mov	di,offset DATA2
    mov	bx,ANS

    mov	al,[si]
    add	al,[di]
    mov	[bx],al

    mov	al,[si+1]
    adc	al,[di+1]
    mov	[bx+1],al

    mov	al,[si+2]
    adc	al,[di+2]
    mov	[bx+2],al

    mov	al,[si+3]
    adc	al,[di+3]
    mov	[bx+3],al

    mov	ax,4c00h
    int	21h

DATA1	dd	0EFFFFFFFh
DATA2	dd	1
ANS	dd	?
ただし結果は表示しないので、デバッガで確認してください。


算術命令

3-4.フラグ

    フラグはCPUの状態などをビット単位で表しています。実際はフラグレジスタ という16bitで管理されているが、実際に使われているのは9つ分、すなわち9bit 分で、残りは使われていない。

CF(キャリーフラグ)
算術演算で繰り上がり(加算)、桁借り(減算)した場合などに「１」しなかった場合は「０」 になる。そのほか、「INT　21h」のシステムコールが正常に処理できなかった場合に「１」に、 正常に処理されれば「０」になることから、サブルーチンを作った際に、エラーフラグとして 用いる人も多い。

PF(パリティーフラグ)
演算結果の最下位バイト(例えばALやBL)を2進数で見たときに、「１」の数が、 偶数なら「１」、奇数なら「０」になる。ほとんど使われないフラグ。

AF(補助フラグ)
BCD演算に使用される詳しくは省略する

ZF(ゼロフラグ)
演算結果が０になった場合に「１」、それ以外なら「０」になる。 よく使うフラグの１つ。

SF(サインフラグ)
サインとは日本語に訳すと「符号」で、演算結果がマイナスになった場合に 「１」になる。アセンブラでマイナスとは、最上位ビットが「１」の時に、マイナス と見なし、「０」の時にプラスト見なす。したがって、このフラグは演算結果の 最上位ビットがそのまま反映されている。詳しくは後で述べる。

IF(割り込み許可フラグ)
ハードウエア割り込みを許可するときは「１」、許可したくない場合や割り込み処理中は 「０」になる。詳しくは10章参照

DF(ディレクションフラグ)
ストリング命令で使用します。詳しくは7章参照

OF(オーバーフローフラグ)
例えば、8bitレジスタALが7Fh(=127)で、さらに1を足すと、80h(=-128)になる。 これは8bitで表せる数は-128～+127までであり、その範囲を超えた計算であるからで、 このような範囲を超えた計算を行った場合に「１」になる。符号付き演算で オーバーフローした際に「１」になる。詳しくは後で述べる。

    フラグは条件分岐で頻繁に使います。
    80286以上ではTSF(タスクスイッチフラグ)などが拡張されています。


算術命令

    3-3.練習問題の解答をフラグとALレジスタの変化をあわせて見てみる。
                           CF PF ZF SF OF AL
    mov	si,offset DATA1    ?  ?  ?  ?  ?  ??
    mov	di,offset DATA2    ?  ?  ?  ?  ?  ??
    mov	bx,ANS             ?  ?  ?  ?  ?  ??

    mov	al,[si]            ?  ?  ?  ?  ?  FF
    add	al,[di]            1  1  1  0  0  00
    mov	[bx],al            1  1  1  0  0  00

    mov	al,[si+1]          1  1  1  0  0  FF
    adc	al,[di+1]          1  1  1  0  0  00
    mov	[bx+1],al          1  1  1  0  0  00

    mov	al,[si+2]          1  1  1  0  0  FF
    adc	al,[di+2]          1  1  1  0  0  00
    mov	[bx+2],al          1  1  1  0  0  00

    mov	al,[si+3]          1  1  1  0  0  EF
    adc	al,[di+3]          0  1  0  1  0  F0
    mov	[bx+3],al          0  1  0  1  0  F0

    mov	ax,4c00h
    int	21h

DATA1	dd	0EFFFFFFFh
DATA2	dd	1
ANS	dd	?
「?」は前の結果が残っているため、不明
最初の「ADD」命令以前
フラグの変わらない「MOV」命令なので、フラグの値は変化しない。

最初の「ADD」命令
  1. FFh+01h=100hはALレジスタに収まりきれず、桁があがっているので、CF(キャリーフラグ)は「１」になる
  2. FFh+01h=100h→AL=00hなので、PF(パリティフラグ)は「０」になる
  3. ALレジスタは00hが入るので、ZF(ゼロフラグ)は「１」になる
  4. ALレジスタは00hで、最上位ビットは「０」。したがってSF(サインフラグ)は「０」になる
  5. 符号なし演算の場合はオーバーフローになったが、この加算を符号なし演算と解釈すると、-1+1=0でオーバーフローしていないので、OF(オーバーフローフラグ)は「０」になる

次の「ADC」命令以前
フラグの変わらない「MOV」命令なので、フラグの値は変化しない。

次の「ADC」命令
FFh+00h+CY(1)=100hなので、最初の「ADD」命令と同じ結果になる

次の「ADC」命令以前
フラグの変わらない「MOV」命令なので、フラグの値は変化しない。

次の「ADC」命令
FFh+00h+CY(1)=100hなので、最初の「ADD」命令と同じ結果になる

最後の「ADC」命令以前
フラグの変わらない「MOV」命令なので、フラグの値は変化しない。

最後の「ADC」命令
  1. EFh+00h+CY(1)=F0hなので、桁あふれしていないのでCFは「０」になる
  2. AL=F0hとなるので、2進数で考えると「11110000」。1の数は4つ。したがってPFは「０」になる
  3. AL≠0なので、ZFは「０」になる
  4. AL=F0hとなるので、2進数で考えると「11110000」で、最上位ビットは「１」。したがってSF(サインフラグ)は「１」になる
  5. 符号付き演算としてみてもオーバーフローしない。したがってOFは「０」になる


算術命令

3-5.引き算

    昔々の世界では、減算命令がないCPUもありました。そこで前にも 説明したように「2の補数」という概念を用いて減算を実行してきました。現在のCPUでは、 どんなに安いCPUにも減算命令は用意されています。
SUB	DEST,SRC

    動作：DEST←DEST-SRC
    影響を受けるフラグ：OF,SF,ZF,AF,CF,PF
    DEST：レジスタ、メモリー
    SRC ：レジスタ、メモリー、即値（ただしメモリー、メモリーの組み合わせは除く）

SBB	DEST,SRC

    動作：DEST←DEST-SRC-CF
    影響を受けるフラグ：OF,SF,ZF,AF,CF,PF
    DEST：レジスタ、メモリー
    SRC ：レジスタ、メモリー、即値（ただしメモリー、メモリーの組み合わせは除く）

    減算命令も加算命令と同様にキャリーフラグを考慮する「SBB」と考慮しない「SUB」の ２つの命令が用意されている。

    「SUB」命令では、DEST(引かれる数)がSRC(引く数)より小さく結果がマイナスになる場合 は加算と逆に、桁を借りてくる。我々が日常計算する場合、引かれる数の方が小さいと、 とりあえず大きい方から小さい方を引いて、頭にマイナスをつける。例えば、「5-10」は 引かれる数の方が小さいので、とりあえず大きい数(10)から小さい数(5)を引いて、 マイナスをつけ「-5」とする。

    アセンブラの場合はもう１桁増やし、その桁が1であると仮定して計算する。例えば、 「5Bh-85h」を計算すると、図のようになる。引かれる数(5Bh)の方が小さいので、 さらに1桁上に「1」があると仮定して、15Bh-85h=D6hとなる。この場合、桁借りをしたので キャリーフラグが1になる。(OF=0,SF=1,ZF=0,他のフラグは省略)

    「SBB」命令では、キャリーフラグが1ならば、さらに1を引く。すなわち前の 引き算の結果、桁借りを起こしていたら、その分も引くことになり、「ADD」命令 の後に、「ADC」命令を実行したのと同様に、「SUB」命令後必要ならば「SBB」命令 を行う。

    「SUB」「SBB」どちらも、フラグは同様に変化し、キャリーフラグは今説明した 通りである。ZFは減算の結果が0であればZF=1、それ以外ならZF=0になる。

    符号付き整数の減算を考えた場合、80h(-127)-1=7Fh(+127)となり、引き算を したはずなのに、結果が大きくなってしまった場合に、オーバーフローを起こしたと言い、 オーバーフローフラグOF=1となる。オーバーフローを起こさなかった場合には0になる。 符号なし整数の場合はOFは無視して構わない。

    符号なし、符号付き整数のどちらも計算結果の上位1bitが1ならばSF=1、上位1bitが 0ならばSF=0となる。そのほかのフラグについては省略する。


算術命令

例）
5(DATA1)-3(DATA2)の結果を表示する
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	dl,DATA1
    sub	dl,DATA2
    add	dl,'0'

    mov	ah,2
    int	21h

    mov	ax,4c00h
    int	21h

DATA1	byte	5
DATA2	byte	3

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです


算術命令

例2）
ダブルワードのDATA1に08000000h、DATA2に1を格納し、DATA1-DATA2の計算結果を ANSに格納する。ただし、計算は1Byte単位で行うものとする。
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	si,offset DATA1
    mov	di,offset DATA2
    mov	bx,ANS

    mov	al,[si]
    sub	al,[di]
    mov	[bx],al

    mov	al,[si+1]
    sbb	al,[di+1]
    mov	[bx+1],al

    mov	al,[si+2]
    sbb	al,[di+2]
    mov	[bx+2],al

    mov	al,[si+3]
    sbb	al,[di+3]
    mov	[bx+3],al

    mov	ax,4c00h
    int	21h

DATA1	dd	08000000h
DATA2	dd	1
ANS	dd	?

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです


算術命令

3-6.かけ算

    足し算、引き算では、足す数（引き数）および足される数（引かれる数）は自由に指定が できた。しかし、かけ算、わり算では、かけられる数（割られる数）は決まっており、 変更できない。(80286以下)　またかける数（割る数）は即値は使用できない。(80286以下)

    かけ算、わり算は比較的新しい命令で、8086以前のCPUや初期のRISCと呼ばれる CPUには無かった命令で、技術的にも困難な命令であったからであるが、技術の 進歩した現在のCPU(80386以上)ではかける数（割る数）、かけられる数（割られる数） ともにメモリーなどが使用できるようになっている。本ホームページでは8086 シリーズと言うことで、8086でも動くアセンブラを対象としているので、 それらについては省く。

MUL	SRC

    動作：符号なし整数の乗算
          SRC = 1byte  :  AX←AL×SRC
          SRC = 1word  :  DX:AX←AX×SRC
          SRC = 1dword :  EDX:EAX←EAX×SRC
    影響を受けるフラグ ： OF,SF,ZF,AF,CF,PF
    SRC ：レジスタ、メモリー

IMUL	SRC

    動作：符号付き整数の乗算
          SRC = 1byte  :  AX←AL×SRC
          SRC = 1word  :  DX:AX←AX×SRC
          SRC = 1dword :  EDX:EAX←EAX×SRC
    影響を受けるフラグ ： OF,SF,ZF,AF,CF,PF
    SRC ：レジスタ、メモリー

    オペランドが8bitの場合は結果は16bitレジスタのAXに代入される。 オペランドが16bitの場合は結果が32bitになり、上位16bitはDXに、下位16bitはAXに 代入される。オペランドが32bitの場合は、結果の上位32bitがEDXに、下位32bitが EAXに代入される(80386以上)。


算術命令

3-7.わり算

DIV	SRC

    動作：符号なし整数の除算
          SRC = 1byte  :  AX ÷ SRC の余り→AH、商→AL
          SRC = 1word  :  DX:AX ÷ SRC の余り→DX、商→AX
          SRC = 1dword :  EDX:EAX ÷ SRC の余り→EDX、商→EAX
    影響を受けるフラグ ： OF,SF,ZF,AF,CF,PF
    SRC ：レジスタ、メモリー

IDIV	SRC

    動作：符号付き整数の除算
          SRC = 1byte  :  AX ÷ SRC の余り→AH、商→AL
          SRC = 1word  :  DX:AX ÷ SRC の余り→DX、商→AX
          SRC = 1dword :  EDX:EAX ÷ SRC の余り→EDX、商→EAX
    影響を受けるフラグ ： OF,SF,ZF,AF,CF,PF
    SRC ：レジスタ、メモリー

    わり算には注意が２つ必要となる。電卓で「１÷０」と入力するとエラーになる。 同様に０で割るとエラーになる。高級言語の場合は０で割るとエラーを表示して プログラムを終了するようにコンパイラが勝手にしてくれる場合もあるが、 アセンブラの場合、通常はOSに処理が移ってしまいどうなるかわからない。

    ２つめは大きい数を小さい数で割ってはいけない。割る数が1byteの場合、割られる 数は1word(=2byte)の数である。そしてその商と余りは1byteである。したがって、 2000h÷2=1000h余り0であるが、商の1000hは1byteには収まりきらない。この場合も エラーが起こり、OSに処理が移ってしまい、その後どうなるかわからない。 このようなエラーを「例外」と呼び、OSが用意している「例外処理」に処理が 移行される。例外については後述する。

  • ０でわり算をしてはならない。
  • 商がレジスタに収まるように注意しなければならない


算術命令

3-8.INC, DEC

INC	DEST

    動作：DEST=DEST+1
    影響を受けるフラグ ： OF,SF,ZF,AF,PF(CFは不変)
    DEST ：レジスタ(セグメントレジスタを除く)、メモリー

DEC	DEST

    動作：DEST=DEST-1
    影響を受けるフラグ ： OF,SF,ZF,AF,PF(CFは不変)
    DEST ：レジスタ(セグメントレジスタを除く)、メモリー

    プログラム中、1足したり、引いたりすることは多い。このためC言語などの 多くのプログラミング言語では、1を足したり引いたりするというのは慣用句的な 命令が用意されている(C言語、JAVAではi++(i--)、++i(--i))。 アセンブラでも用意されており、それが「INC」「DEC」命令である。ただし、 繰り上がりや桁借りをあらわすCF(キャリーフラグ)は変化しない。「ADD」「SUB」 命令よりも若干速く実行することができ、プログラム自体も少し短くなるので、 慣用的にこれを用いる。


分岐

条件分岐

4-1.無条件分岐

    無条件分岐とは、C言語やN88-BASICで言うgotoで、他の処理に移行する命令である。
JMP	DEST

    動作 ：DESTが示す番地に処理を移行する
    DEST : 相対オフセット、即値、メモリ、レジスタ
    影響を受けるフラグ ： なし

    DESTが示す番地に処理を移行します。一般には処理を移行したい場所にラベルを つけて、そのラベルにジャンプするようにします。
    jmp	FUNC2		;FUNC2に処理を移行する
FUNC1:
      :
      :
FUNC2:
      :
      :
    mov	ax,4c00h
    int	21h

    jmp	ax		;AXの内容の番地にジャンプする

    jmp	0		;CSセグメントの0番地にジャンプする

    jmp	ds:100h		;ds:100h番地の内容の番地にジャンプする
    実は、jmp命令には3種類あります。「short　jmp」と「near jmp」、「far　jmp」 である。

short　jmp
現在のアドレスの-128～+127番地以内へのジャンプで、即値を示した場合である。 shortジャンプであるとわかっている場合には「jmp　short　FUNC1」と 記述することによって、アセンブラ(コンパイラ)によっては、実行形式プログラム の大きさが小さくなる場合がある。
near　jmp
同じセグメント内でのジャンプ。上の例では「jmp　ax」「jmp　0」「jmp　ds:100h」が 該当する。ほとんどのアセンブラではこれがデフォルトになっているが、特に 「near　jmp」であることを明記したい場合は「jmp　near　FUNC1」のように 書く。
far　jmp
異なるセグメントへのジャンプ。上の例には示していない。例えば、 「jmp　far　100h:200h」、「jmp　far　[ds:100h]」のように書く。 前者ではセグメント100hの200h番地にジャンプする。後者は、ds:100h の内容が1234h、102h番地の内容が5678hの場合、セグメントが5678hの 1234h番地にジャンプする。


分岐

4-2.比較命令

    条件分岐を行うためには、何か条件を指定しなければならない。アセンブラでの 条件の指定は基本的にフラグしかない。今までは足し算、 引き算でフラグを見てきた。しかし何か条件を見て分岐させたいのは、そういった 演算結果の直後とは限らない。そこで必要になってくるのが比較命令である。 アセンブラでの比較命令はたった１つしかない。それが「CMP」命令である。
CMP	SRC1,SRC2

    動作：SRC1 - SRC2
    影響を受けるフラグ：OF,SF,ZF,AF,CF,PF
    SRC1：レジスタ、メモリー
    SRC2：レジスタ、メモリー、即値（ただしメモリー、メモリーの組み合わせは除く）
    「動作」を見るとわかるように、やっていることは「SUB」命令と同じように、 引き算を行っているだけである。しかし引き算の結果は破棄され、フラグの結果だけが 残る。これにより、SRC1,SRC2の大きさを比べることができる。例えば、同じ値ならば ZF=1になるし、SRC1の方が小さければSF=1になり、SRC1の方が大きければSF=0と いった具合である。このフラグの結果を見て、次のページで 説明する、条件分岐で分岐するわけである。

    条件分岐ではフラグを見るわけであるから、比較命令の直後に条件分岐命令 が置かれるわけであるが、場合によってはフラグの変化しない「MOV」命令などが 置かれることもある。


分岐

4-3.条件分岐

    さて、前のページでも書いたように、アセンブラでの条件分岐は 基本的にフラグによって行われる。したがって、高級言語のように「○○かつ××」とか、 「○○または××」といったような複合条件での分岐はできない。かならずひとつずつ 条件を指定しなければならず、複数の条件の場合は、１つの条件を組み合わせなければならない。

    まず、基本でない（フラグによる条件でない）条件分岐を見てみる。 なぜ基本でないものからかというと、簡単で命令数がすくないからである。
JCXZ	DEST
    動作：CXレジスタが0ならばジャンプする

JECXZ	DEST   (80386以降)
    動作：ECXレジスタが0ならばジャンプする
    条件分岐では、前ページの「無条件分岐」で説明した、 「short」と「near」がある。ただし、「near」は80386以降で追加され、80286以前の CPUでは「short」のみである。ただし、DESTは即値（ラベル）のみしか使用できない。 これは今後出てくる条件分岐命令すべてにあてはまる。

    さて、この「JCXZ」「JECXZ」の用途であるが、まぁ、機能が機能だけに、 きちんと動けば別にどんな用途に用いても良いわけだが、 「LOOP」命令と併せて用いるのが一般的である。「LOOP」命令とは、 (E)CX回処理を繰り返す命令であるが、(E)CX=0の時は、65536回(232回) 繰り返してしまう。これを回避するために用いるのが一般的である。


分岐

    次はフラグを見て分岐する命令です。
ニーモニック 意味 意味 フラグ
JA
JAE
JB
JBE
JC
JE
JG
JGE
JL
JLE
JNA
JNAE
JNB
JNBE
JNC
JNE
JNG
JNGE
JNL
JNLE
JNO
JNP
JNS
JNZ
JO
JP
JPE
JPO
JS
JZ 		Jmup if avobe
Jmup if avobe or equal
Jmup if below
Jmup if below or equal
Jmup if carry
Jmup if equal
Jmup if great
Jmup if great or equal
Jmup if less
Jmup if less or equal
Jmup if not avobe
Jmup if not avobe or equal
Jmup if not below
Jmup if not below or equal
Jmup if not carry
Jmup if not equal
Jmup if not great
Jmup if not great or equal
Jmup if not less
Jmup if not less or equal
Jmup if not overflow
Jmup if not parity
Jmup if not sign
Jmup if not zero
Jmup if overflow
Jmup if parity
Jmup if parity even
Jmup if parity odd
Jmup if sign
Jmup if zero 		上なら（符号なし）
以上なら（符号なし）
下なら（符号なし）
以下なら（符号なし）
Carry Flag=1なら
イコールなら
大きいなら（符号付き）
大きいか同じなら（符号付き）
小さいなら（符号付き）
小さいか同じなら（符号付き）
上じゃないなら＝以下なら（符号なし）
以上じゃないなら＝下なら（符号なし）
下じゃないなら＝以上なら（符号なし）
以下じゃないなら＝上なら（符号なし）
Carry Flag=0 ＝ CF=1じゃないなら
イコールじゃいなら
大きくないなら（符号付き）
大きくも同じでもないなら（符号付き）
小さくないなら（符号付き）
小さくも同じでもないなら（符号付き）
オーバーフローでないなら
Parity Flag=0 ＝ PF=1じゃないなら
マイナスでないなら
ゼロじゃないなら
オーバーフローなら
Parity Flag=1なら
パリティが奇数(PF=1)なら
パリティが偶数(PF=0)
マイナスなら
ゼロなら 		CF=0 and ZF=0
CF=0
CF=1
CF=1 and ZF=1
CF=1
ZF=1
ZF=0 and SF=OF
SF=OF
SF<>OF
ZF=1 or SF<>OF
CF=1 or ZF=1
CF=1
CF=0
CF=0 and ZF=0
CF=0
ZF=0
ZF=1 or SF<>OF
SF<>OF
SF=OF
ZF and SF=OF
OF=0
PF=0
SF=0
ZF=0
OF=1
PF=1
PF=1
PF=0
SF=1
ZF=1
    たくさんあって、ごちゃごちゃしていてわかりずらいかと思います。覚え方としては いくつかあります。英語で覚える人、フラグで覚える人。よく見ると同じものがいくつかあります。 例えば、「JAE」＝「JNB」。意味で考えると、「Jump if above or equal＝以上なら」と 「Jmup if not below＝下でないなら＝以上なら」同じですね。フラグをみると両方「CF=0」 ならジャンプしますから、動作も同じです。アセンブラによってマシン語(2進数の羅列)に 変換されても同じです。これらは人間が意味と照らし合わせて覚えやすいように、 同じ命令を複数用意してあるのです。

    ところで、何が大きかったり、小さかったりするのでしょう？　これは 「CMP」命令と関係しているのです。 「CMP　AX,BX」で、AXの方が○○だったらジャンプするわけです。 「CMP　AX,BX」の次に「JA　100h」とあったら、 AXが上の時にジャンプします。

    さて次に不思議なのが、「JZ」と「JE」。「Jump if Zero」と「Jump if equal」。 フラグはどちらも「ZF=1」の時にジャンプする。なんで「ゼロ」と「イコール」が 同じ意味なんでしょうか？　「CMP」命令を考えると「イコール」はわかりますよね。 前にも書きましたが「CMP」は「SUB」と同じように引き算をして、そのフラグだけを 結果に残す命令なのです。したがって、引き算の結果０になれば、「ゼロ」と考え られますし、同じ値なら（ようするに引いた結果０）「イコール」と考えられます。

    まぁ、よく使うものはそのうち自然と覚えてくるので、別に覚える必要は ないでしょう。


MOV命令

4-4.練習問題

問題：キーボードから２つの１桁の数字を入力し、このかけ算の結果を表示させよ。
ヒント：
  • キーボードからの入力は下記のように行う。ただし、これは１文字分の入力であり、 数字かどうかはチェックしない。自分でチェックしなければならない。
        mov	ah,1
    int	21h

    この時点でALレジスタに入力された文字(のアスキーコード)が入る。
  • 入力された文字は、文字コードである。したがってこのままかけ算を行うことはできない。 文字コードを普通の数値に変換し、計算後に文字コードに戻さなくてはならない。
  • かけ算の結果「9(=39h)」を越える場合がある。この場合は２桁表示させる必要がある。


MOV命令

4-4.練習問題

問題：キーボードから２つの１桁の数字を入力し、このかけ算の結果を表示させよ。
ヒント：
  • キーボードからの入力は下記のように行う。ただし、これは１文字分の入力であり、 数字かどうかはチェックしない。自分でチェックしなければならない。
        mov	ah,1
    int	21h

    この時点でALレジスタに入力された文字(のアスキーコード)が入る。
  • 入力された文字は、文字コードである。したがってこのままかけ算を行うことはできない。 文字コードを普通の数値に変換し、計算後に文字コードに戻さなくてはならない。
  • かけ算の結果「9(=39h)」を越える場合がある。この場合は２桁表示させる必要がある。
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	ah,1
    int	21h		;key in
    sub	al,'0'		;文字コードから数値へ
    cmp	al,9
    ja	BYEBYE		;9より上(数字以外が入力された場合は「BYEBYE」へ飛ぶ)
    mov	bl,al

    mov	ah,2
    mov	dl,0dh
    int	21h
    mov	ah,2
    mov	dl,0ah
    int	21h		;改行

    mov	ah,1
    int	21h		;Key in
    sub	al,'0'		;文字コードから数値へ
    cmp	al,9
    ja	BYEBYE		;9より上(数字以外が入力された場合は「BYEBYE」へ飛ぶ)

    mul	bh		;かけ算の結果がAXに(1桁同士のかけ算は81以下なのでAH=0)
    mov	cl,10
    div	cl		;10で割った結果、商(10の位)がALに、余り(1の位)がAHに入る
    mov	bx,ax

    mov	ah,2
    mov	dl,0dh
    int	21h
    mov	ah,2
    mov	dl,0ah
    int	21h		;改行

    cmp	bl,0
    jz	ICHINOKURAI	;10の位が0ならばジャンプ

    mov	ah,2
    mov	dl,bl
    add	dl,'0'		;文字コードに変換
    int	21h		;10の位表示

ICHINOKURAI:
    mov	ah,2
    mov	dl,bh
    add	dl,'0'		;文字コードに変換
    int	21h		;1の位表示

BYEBYE:
    mov	ax,4c00h
    int	21h		;終了

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです


分岐

4-5.ループ
    他の言語でも大抵ループ命令がある。(C言語では「for」「while」「do...while」、 N88-BASICでは「for....next」)アセンブラでも極々簡単なループ命令がある。
LOOP	SRC

    動作：(E)CX=(E)CX-1(ただしフラグは不変)、(E)CX≠0ならSRCにショートジャンプ
    影響を受けるフラグ：なし
    SRC1：即値
    次のように書き換えも可能である。（ただしフラグは考慮しない）
    dec	cx(ecx)
    loop	SRC

例：'A'を20個表示させる。
START:
    mov	cx,20
    mov	dl,'A'
PL1:
    mov	ah,2
    int	21h
    loop	PL1

    mov	ax,4c00h
    int	21h
    これは上で述べたように、以下のように書き換えることができる。
START:
    mov	cx,20
    mov	dl,'A'
PL1:
    mov	ah,2
    int	21h
    dec	cx
    jnz	PL1

    mov	ax,4c00h
    int	21h
以下、「LOOP」命令での注意を挙げる。
  • ショートジャンプなので、±約128バイトまでしかジャンプできない。
    →減算命令と条件ジャンプを使い、できるだけサブルーチンを作る）
  • (E)CXが0の時は216(232)回ループするので注意する。
    →JCXZ,JECXZで事前に抜ける
  • 処理中で(E)CXレジスタを破壊しない。
    →PUSH,POP命令を用いるか変数に入れる
  • ループ内の処理中に条件分岐(JZ,JA等)でループを抜け出してもかまわない。


分岐

    次にLOOP命令の拡張版(?)を紹介する。アセンブラで唯一複合条件の分岐ができる 命令である。
LOOPZ	SRC
LOOPE	SRC

    動作：(E)CX=(E)CX-1(ただしフラグは不変)、(E)CX≠0で、ZF=1ならSRCにショートジャンプ
    影響を受けるフラグ：なし
    SRC1：即値

LOOPNZ	SRC
LOOPNE	SRC

    動作：(E)CX=(E)CX-1(ただしフラグは不変)、(E)CX≠0で、ZF=0ならSRCにショートジャンプ
    影響を受けるフラグ：なし
    SRC1：即値
    どちらも「LOOP」命令の直前に「CMP」命令や減算命令を置き、その結果 イコール（ゼロ）でかつ、(E)CX≠0ならば繰り返す(LOOPZ,LOOPE)。
イコール（ゼロ）でなくかつ、(E)CX≠0ならば繰り返す(LOOPNZ,LOOPNE)。


分岐

4-6.サブルーチン

    サブルーチンはアルゴリズムのところで 解説しているので、サブルーチンが何かはここでは省略する。

    どこから呼ばれるかわからないサブルーチンでは、戻るアドレスも変わるので、 「JMP」命令を使用することはできない。（できなくはないんだけど） そこでこのような用途には「CALL」と「RET」を用いる。命令から推測できるように、 「CALL」でサブルーチンにジャンプし、「RET」で元の場所に戻ってくる。 元の場所に戻ってくるということは、どこかにアドレスを保存しておかなくては ならない。そのようなレジスタはないのだから、どこかのメモリーに置かれている わけである。これはスタックセグメント(SS)のスタックポインタ(SP)が指す 領域に保管されている。(ちょっと違うけど)

    まず、「CALL」命令が実行されると、SPを２減らす。次に戻ってくるアドレス (CALL命令の次の命令があるアドレス)をSS:SPに保管する。そして、CALL命令のオペランド が指すアドレスにジャンプする
    イメージ的には次のような感じである
    sub	sp,2
    mov	[ss:sp],ip
    jmp	ADDRESS

    逆に「RET」命令では、SS:SPが指す2byte(16bit)の内容のアドレスにジャンプし、 SPを２増やす。

    下記に例を示した。

START:
    mov	dl,'0'
    call	DISP
    inc	dl
    call	DISP
    inc	dl
    call	DISP
    inc	dl
    call	DISP
    inc	dl
    mov	ax,4c00h
    int	21h

DISP:
    mov	ah,2
    int	21h
    ret

    実行結果：「0123」と表示する。

    さて、ジャンプ命令では「short jmp」、「near jmp」と「far jmp」があることは すでに述べた。「CALL」も「near call」と「far call」がある。意味も書き方も同じで、 「call　　far　[ds:1000]」のように書く。「near call」の書き方は上の例に示した 通りである。

    また、サブルーチンから他のサブルーチンもしくは自分自身を呼び出しも可能で、 ある(これはよくやる)。さらに、サブルーチンから他のサブルーチンへ 「jmp」したり、「jmp」で、メインプログラムに戻ってきても良いが、バグを防ぐ目的、 サブルーチンの汎用性といった問題から、お勧めしない。


分岐

    サブルーチンは、汎用性を持たせるように書くことは前に書いた。他のプログラムから 何の手直しもしないで使えるようにするためである。よく使うサブルーチンを作ったら、 自分の「サブルーチン集」を作っておくと便利である。そうすれば前作ったプログラムから コピー＆ペーストで、今作っているプログラムへ持ってこなくてもすむし、一度サブルーチン集を コンパイルしておけば、その部分はコンパイルしなくてすむので、コンパイルも速くできる。

    サブルーチン集を作るには以下のようにする。
  • サブルーチン集の方のファイルの最初に「public　サブルーチン名」を書く
  • そのサブルーチンを呼び出すプログラムの最初に「extern　サブルーチン名:near」
;サブルーチン集
;
public	INPUT
public	PRINT

PRINT:
     :
    処理
     :
    ret

INPUT:
     :
    処理
     :
    ret

;プログラム
;
extern	PRINT:near
extern	INPUT:near

CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
     :
    処理
     :
    call	INPUT
     :
    call	PRINT
     :
    mov	ax,4c00h
    int	21h

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです
         ちなみに、サブルーチン集でなくても、ソースファイルが大きくなってきたら、 ある程度の大きさに分割して、同じようにすると編集も楽だし、コンパイル時間も 速くなる。


分岐

4-7.スタック
    「CALL」命令のアドレス保管の方法を「スタック」 と言い、この仕組みについてもう少し詳しく解説する。

    スタックとは「棚」のことで、棚に物を何も考えずに置いていく（積み重ねていく） ようなイメージである（上図参照）。けれどもこのように積み重ねた物を下から取り出すと、 その上にあった物が崩れてしまうことが日常にもある。したがって上からしか 取り出さないのが普通である。この場合も同様に、上からしか取り出すことが できない。このような保存方式をスタックと言い、その取り出し方法をFirst In Last Out(FILO)と言う。その逆にキューという保存方式があり、その取り出し方法を First In First Out(FIFO)と言う。

    ではこのスタックを保存するのはどこか？　もちろんメモリーなわけだが、これは スタックセグメントのスタックポインタが示す領域(SS:SP)にある。「CALL」命令が 実行される場合を図で示すと下図のようになる。

    次に「RET」が実行された場合を下図に示す。


分岐

4-8.PUSH, POP
    スタック領域は「CALL」「RET」命令のためだけの領域ではない。 スタック領域を使える命令はほかに「PUSH」「POP」命令がある。これは レジスタを一時的に保管する命令と、それを戻す命令である。

PUSH	SRC

    動作：SPを２減らし、SS:SPにSRCを格納
    影響を受けるフラグ：なし
    SRC：レジスタ(16bit、32bitのみ)、セグメントレジスタ、メモリー(16bit、32bitのみ)

POP	DEST

    動作：SS:SPの内容をDESTに代入し、SPを２増やす
    影響を受けるフラグ：なし
    DEST：レジスタ(16bit、32bitのみ)、セグメントレジスタ、メモリー(16bit、32bitのみ)

  • 一時的にレジスタを保存する場合に用いる。
  • サブルーチンではレジスタは保存するのが一般的で、その保存の際によく使われる。
  • 多用するとバグの原因になる。
  • なお、80186(V30)以降は即値(16bit、32bitのみ)をプッシュすることもできるようになった。
  • プッシュしたレジスタ（メモリー）とポップするレジスタは異なっていてもかまわない。
    例えば「mov　ds,es」という命令はない。これを「push　es」「pop　ds」とすれば行うことができる。
    また、「mov　DATA1,DATA2」といったメモリー間の転写はできないが、「push　DATA2」「pop　DATA1」と すればレジスタを破壊せずに行うことができる。
  • AL,AHなど8bitレジスタ、8bitのメモリーをプッシュ、ポップすることはできない。
  • 「CALL」「RET」命令で使われるスタック領域と「PUSH」「POP」命令で使われるスタック領域は 同じ領域なので、「push　100h」「ret」とすると、100h番地にジャンプすることと同じことになる。 逆に「CALL」で飛んできたサブルーチンの最初に「POP」命令を用いると、呼ばれたアドレスの次の 命令のあるアドレスの番地がわかる。
    →サブルーチン内で「PUSH」と「POP」が対になっていないと「RET」命令でどこに 戻るかわからない（大抵の場合暴走する）。


分岐

    「PUSH」「POP」関連の命令はあと4対ある。
PUSHA

    動作：PUSH　AX、PUSH　CX、PUSH　DX、PUSH　BX、PUSH　SP、PUSH　BP、
          PUSH　SI、PUSH　DIをこの順に実行する
    影響を受けるフラグ：なし

POPA

    動作：POP　DI、POP　SI、POP　BP、POP　SP、POP　BX、POP　DX、
          POP　CX、POP　AXをこの順に実行する。
    影響を受けるフラグ：なし

PUSHAD

    動作：PUSH　EAX、PUSH　ECX、PUSH　EDX、PUSH　EBX、PUSH　ESP、PUSH　EBP、
          PUSH　ESI、PUSH　EDIをこの順に実行する
    影響を受けるフラグ：なし

POPAD

    動作：POP　EDI、POP　ESI、POP　EBP、POP　ESP、POP　EBX、POP　EDX、
          POP　ECX、POP　EAXをこの順に実行する。
    影響を受けるフラグ：なし
これらは、サブルーチン内でレジスタを保存する場合などに用いられる。

PUSHF

    動作：SPを２減らし、SS:SPにFLAGSを格納
    影響を受けるフラグ：なし

POPF

    動作：SS:SPの内容をFLAGSに代入し、SPを２増やす
    影響を受けるフラグ：なし

PUSHFD

    動作：SPを4減らし、SS:SPにEFLAGSを格納
    影響を受けるフラグ：なし

POPFD

    動作：SS:SPの内容をEFLAGSに代入し、SPを4増やす
    影響を受けるフラグ：なし

    フラグの保存は、例えば「CMP」命令後、ほかの処理をしてから条件分岐を させたい場合や、DF(7章参照)をサブルーチン内で 保存したい場合などに用いる。


MOV命令

4-9.練習問題

問題
文字列を入力するサブルーチンを作れ。文字列の最後には0Dhが格納するものとする。
呼び出す条件として、BXには入力した文字列を格納するバッファーのアドレス、 CXには最大入力文字数-1が入っているものとし、リターン情報として、 AXレジスタに文字数(0Dhは除く)を入れ、そのほかのレジスタは保存する。
また、BS(バックスペース)も有効とする。

ヒント：
  • BSの文字コードは08hである。ただし、BSを表示しただけではカーソルが戻るだけなので、 スペースを表示し、もう一度カーソルを戻す(08hを表示させる)必要がある。
  • バックスペースを有効にするために、入力した文字がすぐに表示されては困る。 この場合はAH=8の状態で「INT　21h」を行うと、ALレジスタに入力された文字コードが 格納される。しかしAH=2と違い、入力された文字は表示されない(C言語でいう「getch()」)。
解答
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	bx,offset DATA
    mov	cx,50
    call	LINEINP

    mov	ah,2
DISP:
    mov	dl,[bx]
    int	21h		;表示
    inc	bx
    cmp	dl,0dh
    jnz	DISP

    mov	ax,4c00h
    int	21h
DATA	db	51 dup(?)	;バッファー
;+------------------------
;|１行入力
;|最後には 0Dh が入る
;+------------------------
;|in  bx <= buffer(offset)
;|    cx <= 最大文字数
;+------------------------
;|out ax <= 入力文字数
;|          (改行なし)
;+------------------------
;|break ; ax
;+------------------------
LINEINP	proc			;サブルーチン開始

           :
           :
    ;ここに処理を書く
           :
           :
    endp			;サブルーチン終了
    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです


MOV命令

4-9.練習問題

問題
文字列を入力するサブルーチンを作れ。文字列の最後には0Dhが格納するものとする。
呼び出す条件として、BXには入力した文字列を格納するバッファーのアドレス、 CXには最大入力文字数-1が入っているものとし、リターン情報として、 AXレジスタに文字数(0Dhは除く)を入れ、そのほかのレジスタは保存する。
また、BS(バックスペース)も有効とする。

ヒント：
  • BSの文字コードは08hである。ただし、BSを表示しただけではカーソルが戻るだけなので、 スペースを表示し、もう一度カーソルを戻す(08hを表示させる)必要がある。
  • バックスペースを有効にするために、入力した文字がすぐに表示されては困る。 この場合はAH=8の状態で「INT　21h」を行うと、ALレジスタに入力された文字コードが 格納される。しかしAH=2と違い、入力された文字は表示されない(C言語でいう「getch()」)。
解答
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	bx,offset DATA
    mov	cx,50
    call	LINEINP

    mov	ah,2
DISP:
    mov	dl,[bx]
    int	21h		;表示
    inc	bx
    cmp	dl,0dh
    jnz	DISP

    mov	ax,4c00h
    int	21h
DATA	db	51 dup(?)	;バッファー
;+------------------------
;|１行入力
;|最後には 0Dh が入る
;+------------------------
;|in  bx <= buffer(offset)
;|    cx <= 最大文字数
;+------------------------
;|out ax <= 入力文字数
;|          (改行なし)
;+------------------------
;|break ; ax
;+------------------------
LINEINP	proc
    push	dx
    push	di
    inc	cx
    mov	di,0
INPUT:	mov	ah,8
    int	21h

    cmp	al,8		;BSか？
    jz	_BS

    mov	[bx+di],al

    cmp	al,0dh		;リターンキーか？
    jz	_RET

    cmp	di,cx		;文字数制限大丈夫？
    jz	OVER
    inc	di

DISPC:	mov	dl,al
    mov	ah,2
    int	21h		;disp char

    jmp	short INPUT

OVER:
    mov	[di+bx-1],al	;入力可能文字数を超えていた場合の処理
    push	ax
    mov	ah,2
    mov	dl,8
    int	21h		;disp BS
    pop	ax
    jmp	short DISPC

_BS:	or	di,di
    jz	INPUT		;何も入力されていなかったらBS無効

    mov	ah,2
    mov	dl,8
    int	21h		;disp BS
    mov	ah,2
    mov	dl,' '
    int	21h		;disp SP
    mov	ah,2
    mov	dl,8		;disp BS
    int	21h
    dec	di
    jmp	short INPUT

_RET:
    mov	ah,2
    mov	dl,0dh
    int	21h
    mov	ah,2
    mov	dl,0ah
    int	21h		;改行

    mov	ax,di
    pop	di
    pop	dx
    dec	cx
    ret
    endp
    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです


論理演算

    論理演算とは算術演算と異なり、真か偽か２つの状態から一定の規則に従って結果を 得る計算方法である。２つの状態ということは、真と偽を２進数の1と0に置き換えて 考えれば、２進数の１桁で表すことができる。一方、レジスタやメモリーの最小単位は 8bit(1byte)であるので、8bit(1byte)で、真か偽かの状態を８個記憶することができる。 したがってこれから紹介する命令では最小単位が8bitであるから、８桁の２進数になおして 計算することになる。

5-1.AND命令

    AND命令は「論理積」を表す。論理積とは「両方とも1ならば結果は1、それ以外の場合の 結果は0」である。(0×0=0, 0×1=0, 1×0=0, 1×1=1であるから各ビットの積と考える こともできる)

    これを表にすると下のようになる。(このような表を真理値表という)
DEST | SRC | 結果(DEST)
0 | 0 | 0
0 | 1 | 0
1 | 0 | 0
1 | 1 | 1

AND	DEST,SRC

    動作：DEST←DEST and SRC
    影響を受けるフラグ：OF=0,CF=0,ZF,SF,PF,AF
    DEST：レジスタ、メモリー
    SRC ：レジスタ、メモリー、即値（ただしメモリー、メモリーの組み合わせは除く）

    フラグはCF,OFがクリアされる。AFは不定。結果が0になった場合ZF=1、それ以外はZF=0。 上位1bitがSFに反映される。PFは結果に応じてセットされる。

用途
・0でのマスク
マスクとは、いらないビットを0または1で埋めることで、必要な部分を抜き出すことをいう。 0でのマスクとは、必要のない桁を0にすることをいう。 例えば4bit目と5bit目以外を0でマスクしたい場合には、次のようにする。

    and	al,00011000b

al=78h(=11110000b)の場合、
11110000
00011000
00010000
となり、4bit目と5bit目以外は0になり、4bit目と5bit目は元のデータのままに なっていることがわかる。

・0かどうかのチェック
同じレジスタ同士の論理積をとると0かどうかのチェックをすることができる。 例えば、AL=78h(=11110000b)のとき、
11110000
11110000
11110000
AL=0(=00000000b)の時は
00000000
00000000
00000000
したがってどちらの場合もレジスタの値は変わらない。しかし下の例ではゼロフラグが セットされる。したがって「CMP　AL,0」と（レジスタの値とゼロフラグは）同じ結果が 得られる。レジスタが8bit,32bitの時は「AND」命令で0かどうかのチェックをするメリットは ないが、16bitレジスタの場合は命令のサイズが1バイト小さくなり、さらにCPUが80286以下の CPUでは若干処理速度も速くなる。ただし、なぜだかわからないが、0かどうかのチェックには 「OR」命令を用いるのが一般的である。

・正か負かのチェック
上の例と同様に正か負かのチェックをしたい場合は「AND」命令を用いることができ、 同様のメリットが生まれる。


論理演算

5-2.OR命令

    OR命令は「論理和」を表す。論理和とは「どちらか一方が1ならば結果は1、両方とも0の 場合は0」である。(0+0=0, 0+1=1, 1+0=1, 1+1=1以上であるから各ビットの和と考える こともできなくはない)

    これを表にすると下のようになる。(このような表を真理値表という)
DEST | SRC | 結果(DEST)
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 1

OR	DEST,SRC

    動作：DEST←DEST or SRC
    影響を受けるフラグ：OF=0,CF=0,ZF,SF,PF,AF
    DEST：レジスタ、メモリー
    SRC ：レジスタ、メモリー、即値（ただしメモリー、メモリーの組み合わせは除く）

    フラグはCF,OFがクリアされる。AFは不定。結果が0になった場合ZF=1、それ以外はZF=0。 上位1bitがSFに反映される。PFは結果に応じてセットされる。

用途
・1でのマスク
マスクとは、いらないビットを0または1で埋めることで、必要な部分を抜き出すことをいう。 1でのマスクとは、必要のない桁を1にすることをいう。 例えば4bit目と5bit目以外を1でマスクしたい場合には、次のようにする。

    or	al,11100111b

al=78h(=11110000b)の場合、
11110000
11100111
11110111
となり、4bit目と5bit目以外は1になり、4bit目と5bit目は元のデータのままに なっていることがわかる。

・0かどうかのチェック
同じレジスタ同士の論理和をとると0かどうかのチェックをすることができる。 例えば、AL=78h(=11110000b)のとき、
11110000
11110000
11110000
AL=0(=00000000b)の時は
00000000
00000000
00000000
したがってどちらの場合もレジスタの値は変わらない。しかし下の例ではゼロフラグが セットされる。したがって「CMP　AL,0」と（レジスタの値とゼロフラグは）同じ結果が 得られる。レジスタが8bit,32bitの時は「OR」命令で0かどうかのチェックをするメリットは ないが、16bitレジスタの場合は命令のサイズが1バイト小さくなり、さらにCPUが80286以下の CPUでは若干処理速度も速くなる。「AND」命令でも同様の結果が 得られるが、なぜか0かどうかのチェックにはこの論理和を用いるのが一般的である。

・正か負かのチェック
上の例と同様に正か負かのチェックをしたい場合は「OR」命令を用いることができ、 同様のメリットが生まれる。


論理演算

5-3.XOR命令

    XOR命令は「排他的論理和」を表す。排他的論理和とは「両方とも同じなら0、異なる 場合は0」である。

    これを表にすると下のようになる。(このような表を真理値表という)
DEST | SRC | 結果(DEST)
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0

XOR	DEST,SRC

    動作：DEST←DEST xor SRC
    影響を受けるフラグ：OF=0,CF=0,ZF,SF,PF,AF
    DEST：レジスタ、メモリー
    SRC ：レジスタ、メモリー、即値（ただしメモリー、メモリーの組み合わせは除く）

    フラグはCF,OFがクリアされる。AFは不定。結果が0になった場合ZF=1、それ以外はZF=0。 上位1bitがSFに反映される。PFは結果に応じてセットされる。

用途
    XORは次のような用途に使われる

・ビットの反転
例えば4bit目と5bit目を反転したい場合、次のようにする

    xor	al,00011000b

al=78h(=11110000b)の場合、
11110000
00011000
11101000
となり、4bit目と5bit目が反転されていることがわかる。

・データの暗号化
同じ値で２度排他的論理和をとるともとの値に戻る。これを利用することで簡単なデータの 暗号化を行うことができる。
例えば、78h(=11110000b)を暗号キー53h(=01010011b)で暗号化すると、
11110000
01010011
10100011
10100011(=A3h)が得られる。これをもう一度、暗号キー53h(010100011)で復元すると
10100011
01010011
11110000
11110000b(=78h)が得られ、これは最初の値と同じである。このように、 暗号化、復元が簡単にできる。

・レジスタへの0の代入
下のようにSRCとDESTに同じレジスタを用いるとそのレジスタは0になる。

    xor	al,al

例えば、AL=78h(=11110000b)の場合、
11110000
11110000
00000000
この方法は、レジスタが8bit,32bitの時はメリットはないが、16bitレジスタの場合は 命令のサイズが1バイト小さくなる。しかしCPUが8086の場合、若干処理速度が遅くなる。 頻繁的に使われる方法である。


論理演算

5-4.NOT命令

    NOT命令は「1の補数」を表す。「1の補数」とは「0なら1に、1なら0に反転すること」 である。

    これを表にすると下のようになる。(このような表を真理値表という)
DEST | 結果(DEST)
0 | 1
1 | 0

NOT	DEST

    動作：DEST←not DEST
    影響を受けるフラグ：なし
    DEST：レジスタ、メモリー


論理演算

5-5.NEG命令

    NEG命令は「2の補数」を表す。「2の補数」とは「符号付き整数の符号の反転」 である。この命令は論理演算ではないが、「NOT」と同じ、補数命令であるので、この章に 追加した。

NEG	DEST

    動作：DEST←DESTの符号を反転させる
    影響を受けるフラグ：なし
    DEST：レジスタ、メモリー

どのような動作をするかをみると、まずDESTの「NOT」をとり、1を（算術演算で）加える。 -45h(=10111011b)を例に取ると、
10111011(=-45h)
↓(NOT)
01000100(=+44h)
↓(+1)
01000101(=+45h)
+45hとなり、符号が反転したのがわかるだろう。 では逆に、+45h(01000101b)の2の補数を取ってみてください。


論理演算

5-6.TEST命令

    TEST命令は「論理積」を表す。すなわち「AND」命令と同じである。 何が違うかというと、下に示したようにDESTがなく、結果は保持されないことである。では 何が変化するかというと、フラグである。２つのオペランドの論理積をとり、そのフラグだけ 反映する命令である。

TEST	SRC1,SRC2

    動作: SRC1 and SRC2 のフラグのみ反映
    影響を受けるフラグ：OF=0,CF=0,ZF,SF,PF,AF
    SRC1：レジスタ、メモリー
    SRC2：レジスタ、メモリー、即値（ただしメモリー、メモリーの組み合わせは除く）

用途
・ビットの検査
あるビットが1ならばジャンプする例
      :
      :
    test	al,00011000b
    jnz	XXX45:		;ALの4,5bitがともに1ならジャンプ
    test	al,00000001b
    jnz	XXX01;		;ALの下位1bitが1ならジャンプ
      :


論理演算

5-7.練習問題

  1. キーボードから１文字入力し、アルファベットならば大文字に変換して表示するプログラムを 作れ。ただしリターンキーが押されるまで繰り返す。
  2. キーボードから１文字入力し、アルファベットの大文字なら小文字に、小文字なら大文字に 変換して表示するプログラムを作れ。ただしリターンキーが押されるまで繰り返す。

ヒント
  • キーが入力されても表示されては困る(エコーバックしない)。以前紹介したが、今回も 「AH=8」を使用する。(入力文字はALに返される)
  • 'A'～'Z'の文字コードは41h～5Ah。'a'～'z'の文字コードは61h～7ah。20h分の差が ある。「SUB」命令、「ADD」命令使うの方法もあるが、今回は論理演算を用いる。
  • リターンキーが押されると、AL=0Dhが返される。


論理演算

5-7.練習問題

  1. キーボードから１文字入力し、アルファベットならば大文字に変換して表示するプログラムを 作れ。ただしリターンキーが押されるまで繰り返す。
  2. キーボードから１文字入力し、アルファベットの大文字なら小文字に、小文字なら大文字に 変換して表示するプログラムを作れ。ただしリターンキーが押されるまで繰り返す。

ヒント
  • キーが入力されても表示されては困る(エコーバックしない)。以前紹介したが、今回も 「AH=8」を使用する。(入力文字はALに返される)
  • 'A'～'Z'の文字コードは41h～5Ah。'a'～'z'の文字コードは61h～7ah。20h分の差が ある。「SUB」命令、「ADD」命令使うの方法もあるが、今回は論理演算を用いる。
  • リターンキーが押されると、AL=0Dhが返される。

解答1:大文字に変換
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	ah,8
    int	21h		;文字入力

    cmp	al,0dh
    jz	EXIT		;リターンキーなら終了

    cmp	al,'a'		;↓小文字かどうかのチェック
    jb	DISP
    cmp	al,'z'
    ja	DISP		;↑ここまで

    and	al,11011111b	;to upper charactar

DISP:
    mov	dl,al
    mov	ah,2
    int	21h		;表示

    jmp	short START

EXIT:
    mov	ax,4c00h
    int	21h		;終了

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

解答2:大文字、小文字を逆転
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	ah,8
    int	21h		;文字入力

    cmp	al,0dh
    jz	EXIT		;リターンキーなら終了

    cmp	al,'A'		;↓アルファベットかどうかのチェック
    jb	DISP
    cmp	al,'Z'
    jbe	XCHANGE
    cmp	al,'a'
    jb	DISP
    cmp	al,'z'
    ja	DISP		;↑ここまで
XCHANGE:
    xor	al,00100000b	;大文字、小文字逆転

DISP:
    mov	dl,al
    mov	ah,2
    int	21h		;表示

    jmp	short START

EXIT:
    mov	ax,4c00h
    int	21h		;終了

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです


シフト演算

    シフト演算とは論理演算と同じようにビットを操作する命令である。具体的には、 2進数の各ビットをずらすことである。ローテート命令は循環(回転)させる命令である。

6-1. シフト
    シフト命令は上で説明したように、ビットをずらす命令である。左にずらすか、右に ずらすか、ずらすデータが符号付き整数か、符号なし整数（もしくは数値以外のデータ）かによって ４種類のソフト命令が用意されている。

    まず、符号なし整数（もしくは数値以外のデータ）のシフト命令について説明する。 これらは論理シフトと言い、ずらしてはみ出す１桁はキャリーフラグ(CF)に格納される。 足りなくなる１桁には無条件に0が入る(下図参照)。

SHR	DEST,Count
SHL	DEST,Count

    動作：別途解説
    影響を受けるフラグ：OF(Count=1の場合のみ結果を反映),
                        SF,ZF,PF(結果を反映),
                        CF:別途解説
    DEST  ：レジスタ、メモリー
    Count ：CL、即値(8bit以下)
    シフト命令はCount(CLまたは8bitの符号なし整数で表せる範囲の即値)で指定した分だけ、 上述のシフトを行う。CFには、はみ出した桁が格納されるが、Countの値がDESTのサイズを 越えた場合(「SHR　AX,17」など)は、CFの値は未定である。上のニーモニックにも示したが、 OFはCount=1nの時のみ結果を反映するが、Count=1の時もOFを見て何かするようなことは あまりない。

    次に、符号付き整数のシフトを説明する。符号付き整数は、上位1bitが符号を表している。 この符号を壊さないようにシフトさせる(下図参照)。この理由は後述する。

    上図のようにSHLとSALは同じ働きをする。しかしSARはSALと異なり、上位1bitには0が入る のではなく、ずらす前の値がそのまま残る。
SAR	DEST,Count
SAL	DEST,Count

    動作：別途解説
    影響を受けるフラグ：OF(Count=1の場合のみ結果を反映),
                        SF,ZF,PF(結果を反映),
                        CF:別途解説
    DEST  ：レジスタ、メモリー
    Count ：CL、即値(8bit以下)

    さて、シフトを行うとデータはどうなるのだろうか？まず、左にずらした時(SAL, SHL)の例を示す。

    このように左にずらすと、その値は2倍になる。

    次に、8bitの128を右にずらすことを考える。8bitの128は符号付き整数で考えると -128である。この両者をSHRとSARでシフトさせると下図のようになる。

    右にずらす場合、SHRとSARでこのような差がでる。もし符号付き整数の-128を 「SHR」で右にずらすと、-128→+64となる。これを「SAR」でずらすと-128→-64と なり、1/2になる。このようにその値が符号付き整数の場合は「SAR」を使い、 それ以外の場合は「SHR」を使うように、使い分ける必要がある。


シフト演算

6-2.ローテート
    ローテート命令とは、前ページで説明したように、循環させる命令である。
RCL	DEST,Count
RCR	DEST,Count
ROL	DEST,Count
ROR	DEST,Count

    動作：別途解説
    影響を受けるフラグ：OF(Count=1の場合のみ結果を反映),
                        CF:別途解説
                        その他のフラグは不変
    DEST  ：レジスタ、メモリー
    Count ：CL、即値(8bit以下)
    ROL,RORは下図のようにローテートし、あふれた1bitはCFと下位(上位)に入る。 RCL,RCRはキャリーフラグを含めたローテートとなる。


シフト演算

6-3.練習問題1

問題
キーボードから１文字入力し、その文字コードと、その文字コードを左右反転させた 文字コードを表示せよ。
例：'A'を入力したら、「A:41h->82h」と表示する。


シフト演算

6-3.練習問題1

問題
キーボードから１文字入力し、その文字コードと、その文字コードを左右反転させた 文字コードを表示せよ。
例：'A'を入力したら、「A:41h->82h」と表示する。

START:
    mov	ah,1
    int	21h		;入力
    push	ax		;入力文字ストック

    mov	ah,2
    mov	dl,':'
    int	21h		;出力
    call	BYTEHEX
    call	DISP
    mov	ah,2
    mov	dl,'h'
    int	21h
    mov	ah,2
    mov	dl,'-'
    int	21h
    mov	ah,2
    mov	dl,'>'
    int	21h

    pop	ax		;入力文字ポップ
    call	REVERSE		;逆にする
    mov	al,dl
    call	BYTEHEX
    call	DISP

    mov	ax,4c00h
    int	21h		;終了

DISP:				;AH,ALを表示
    mov	dx,ax
    push	dx
    mov	dl,dh
    mov	ah,2
    int	21h		;10hの位表示
    pop	dx
    mov	ah,2
    int	21h		;1hの位表示
    ret

BYTEHEX:				;ALを表示できるように変換し、
                ;AHとALに格納する
    mov	ah,al
    shr	ah,4
    and	ah,00001111b
    add	ax,3030h
    cmp	ah,'9'
    jbe	BYTEHEX_1
    add	ah,7
BYTEHEX_1:
    cmp	al,'9'
    jbe	BYTEHEX_2
    add	al,7
BYTEHEX_2:
    ret

REVERSE:
    mov	cx,8
REVERSE_1:
    sal	al,1
    rcr	dl,1
    loop	REVERSE_1
    ret


シフト演算

6-4.練習問題2

問題
１文字入力して、その文字コードを2進数で表示するプログラムを作れ。


シフト演算

6-4.練習問題2

問題
１文字入力して、その文字コードを2進数で表示するプログラムを作れ。

START:
    mov	ah,1
    int	21h		;入力
    push	ax		;入力文字プッシュ

    mov	ah,2
    mov	dl,0Dh
    int	21h
    mov	ah,2
    mov	dl,0Ah
    int	21h		;改行
    pop	ax		;入力文字ポップ

    mov	cx,8
HEX2BIN:
    xor	dl,dl
    shl	al,1
    adc	dl,'0'

    mov	ah,2
    int	21h
    loop	HEX2BIN

    mov	ax,4c00h
    int	21h		;終了


ストリング命令

7-1.ストリング命令とは

    ストリング(String)とは直訳すると紐とか糸という意味である。コンピュータ関連では、 糸のようにつながった（連続した）メモリー空間を指す。(場合によっては「文字列」を指す人も いるようだが、ちょっと違う。)

    ストリング命令とは連続したメモリーの転送（コピー）、比較などを行う命令である。 転送、比較の単位は１バイト、１ワード、１ダブルワード(32bitCPU)である。

    ストリング命令自体は、１単位の転送、比較しかできない。したがって今まででてきた 「MOV」命令や「CMP」命令と対して変わらない。もっと言えば、今までの命令の方が 高機能である。しかしリピートプリフィックス命令「REP」などと組み合わせて使うことで、 便利な命令に変身してしまう。

    ストリング命令は次ページから詳しく解説するが、次のような基本仕様がある。

  • データの読み出しは[DS:SI]からのみである。
  • データの書き出しは[ES:DI]へのみである。
  • 使えるレジスタは(EAX),AX,ALのみである。

なお、80186以上で使われる「INS」と「OUTS」は8-4で説明する。


ストリング命令

7-2.MOVS?

MOVSB

    動作：[DS:SI]の内容1バイトを[ES:DI]に転送し、
          DF=1の時は、SI,DIを1減らす。
          DF=0の時は、SI,DIを1増やす。
    影響を受けるフラグ：なし

MOVSW

    動作：[DS:SI]の内容2バイトを[ES:DI]に転送し、
          DF=1の時は、SI,DIを2減らす。
          DF=0の時は、SI,DIを2増やす。
    影響を受けるフラグ：なし

MOVSD	(80386以上)

    動作：[DS:SI]の内容4バイトを[ES:DI]に転送し、
          DF=1の時は、SI,DIを4減らす。
          DF=0の時は、SI,DIを4増やす。
    影響を受けるフラグ：なし
    上のニーモニック表の通り、「MOVS?」命令は転送を行う。メモリーの転送後、 SI,DIが増えるか減るかは、DF(フラグ)によって変わる。DFの設定は 後述する。また、この命令だけでは１度に１バイト、２バイト、４バイトしか転送 できない。これでは不便なので、次ページの「REP」命令で、 連続して転送を行う。


ストリング命令

7-3.REP

    「MOVS?」命令だけでは１単位の転送しかできないため、ある領域の転送を行いたい 場合にはこの「REP」命令と組み合わせて使う。
例：REP　MOVSB

    動作：[DS:SI]の内容1バイトを[ES:DI]に転送し、
          DF=1の時は、SI,DIを1減らす。
          DF=0の時は、SI,DIを1増やす。
          これをCX回繰り返す。
    影響を受けるフラグ：なし
    したがって、100バイトのメモリーの転送を行いたい場合は、CX=100の状態で、「REP　MOVSB」 を実行すればよい。CXは何回繰り返すかであり、バイト数ではない 。したがって、CX=100の状態で「REP　MOVSW」を実行すると200バイトの メモリーが転送される。またCX=0の時に「LOOP」を実行すると65536回 繰り返されたが、「REP」命令は１回も実行されない。

例）
    mov	si,1000h
    mov	di,2000h
    mov	cx,100
    rep	movsb
というプログラムを「DF=1」の時に実行すると、900h～1000hの内容が1900h～2000hにコピーされる。 (SI,DIが減るため)

    ところが同じプログラムを「DF=0」の時に実行すると、1000h～1100hの内容が2000h～2100hに コピーされる。(SI,DIが増えるため)

    DFの設定の仕方は次ページに譲るとして、「REP」の兄弟を 紹介する。「REPE(REPZ)」と「REPNE(REPNZ)」である。これは後述するストリング命令 「SCAS?」、「CMPS?」と組み合わせて 使う。「SCAS?」「CMPS?」は後述するように比較命令である。比較命令と言うことは お察しの通り、フラグが変化するのである。この「REPE(REPZ)」と「REPNE(REPNZ)」は ゼロフラグ(ZF)とCXの値を見て終了するか、繰り返すかを決める。どう決めるかは下表の 通りである。
繰り返しプリフィックス命令 | 終了条件 | 終了条件
REP | CX=0 | ZF=0(ただし、比較命令の時のみ)
REPE(REPZ) | CX=0 | ZF=0(ただし、比較命令の時のみ)
REPNE(REPNZ) | CX=0 | ZF=1(ただし、比較命令の時のみ)

    この表で、２つの終了条件はどちらかが成り立てば終了するものである。

    この表の通り、実は「REP」と「REPE(REPZ)」は同じものである。ただしストリング命令が 比較命令の場合は「REPE(REPZ)」を用い、転送命令の場合は「REP」を用いるのが一般的で ある。

    なお、これらの命令の直後にストリング命令がない場合は、無視されます。


ストリング命令

7-4.DFの設定

    何度も書いているように、ストリング命令はDFと密接に関係している。DFの設定が 間違えば、違う領域をコピーしたりするため、容易に暴走したり、他のデータを破壊してしまう ことも頻繁に起こる。

    例えば、下のプログラムのように、ある領域からある領域にデータをコピーする場合は 「DF=0」である必要がある。
    cld

      :
      :
    mov	si,offset DATA1
    mov	di,offset DATA2
    mov	cx,di
    sub	cx,si
    rep	movsb
      :
      :
DATA1	db	"ここにはコピーもとのデータが入る"
DATA2	db	100 dup(0)
    このプログラムの１番上にある「CLD」がDF=0にする命令である。
CLD
    動作：DF=0
    影響を受けるフラグ：DF=0

STD
    動作：DF=1
    影響を受けるフラグ：DF=1
    メモリーの上位から下位に転送や比較を行う場合は「DF=1」すなわち「STD」を行い、 下位から上位に転送や比較を行う場合は「DF=0」すなわち「CLD」を行う。


ストリング命令

7-5.LODS?

LODSB

    動作：[DS:SI]の内容1バイトをALに読み込み、
          DF=1の時は、SIを1減らす。
          DF=0の時は、SIを1増やす。
    影響を受けるフラグ：なし

LODSW

    動作：[DS:SI]の内容2バイトをAXに読み込み、
          DF=1の時は、SIを2減らす。
          DF=0の時は、SIを2増やす。
    影響を受けるフラグ：なし

LODSD	(80386以上)

    動作：[DS:SI]の内容4バイトをEAXに読み込み、
          DF=1の時は、SIを4減らす。
          DF=0の時は、SIを4増やす。
    影響を受けるフラグ：なし
    上のニーモニック表の通り、「LODS?」命令はメモリーの内容をレジスタに読み込む。 SIが増えるか減るかは、「MOSV?」と同様、DF(フラグ)によって 変わる。この命令は、読み込んだ値を細工して(例えば大文字に変換したり)使ったり、 比較して(例えば大文字かなど)使うことが多いので、「REP」命令] と組み合わせて使うことはほとんどない。どちらかと言えば、細工や比較した後に、 「STOS?」で格納して、「LOOP」命令で 繰り返すことが多い。例えば、文字列を大文字にしながらコピーするとか、ある文字が 出てくるまでコピーすると言った用途に使われる。


ストリング命令

7-6.STOS?

STOSB

    動作：ALを[ES:DI]に書き込み、
          DF=1の時は、DIを1減らす。
          DF=0の時は、DIを1増やす。
    影響を受けるフラグ：なし

STOSW

    動作：AXを[ES:DI]に書き込み、
          DF=1の時は、DIを2減らす。
          DF=0の時は、DIを2増やす。
    影響を受けるフラグ：なし

STOSD	(80386以上)

    動作：EAXを[ES:DI]に書き込み、
          DF=1の時は、DIを4減らす。
          DF=0の時は、DIを4増やす。
    影響を受けるフラグ：なし
    上のニーモニック表の通り、「STOS?」命令はレジスタの内容をメモリーに書き込む。 DIが増えるか減るかは、「MOSV?」と同様、DF(フラグ)によって 変わる。

    この命令は前ページの「」、「 LOOP」 と組み合わせて、例えば大文字に変換するといった細工や大文字かなど比較を行いながらの コピーに使われたり、文字列を大文字にしながらコピーするとか、ある文字が 出てくるまでコピーすると言った用途に使われる。

    別の用途としては、メモリーの初期化に使われる。例えば、ALに初期値を格納し、 CXに初期化したいメモリーのバイト数を入れ、「REP」命令と 組み合わせて、メモリーを初期値(AL)で埋めたりする。


ストリング命令

7-7.SCAS?

SCASB

    動作：ALと[ES:DI]とを比較する
          DF=1の時は、DIを1減らす。
          DF=0の時は、DIを1増やす。
    影響を受けるフラグ：OF,SF,ZGF,AF,PF,CFが結果を反映する

SCASW

    動作：AXと[ES:DI]とを比較する
          DF=1の時は、DIを2減らす。
          DF=0の時は、DIを2増やす。
    影響を受けるフラグ：OF,SF,ZGF,AF,PF,CFが結果を反映する

SCASD	(80386以上)

    動作：EAXと[ES:DI]とを比較する
          DF=1の時は、DIを4減らす。
          DF=0の時は、DIを4増やす。
    影響を受けるフラグ：OF,SF,ZGF,AF,PF,CFが結果を反映する
    上のニーモニック表の通り、「SCAS?」命令はレジスタの内容とメモリーの内容を比較する。 DIが増えるか減るかは、「MOSV?」と同様、DF(フラグ)によって 変わる。

    この命令は主に、次の用途に使われる。
  • 「REPNE(REPNZ)」と組み合わせ、AL(AX,EAX)と同じ文字を探す。
  • 「REPE(REPZ)」と組み合わせ、AL(AX,EAX)と違う文字を探す。
    ここで注意してほしいのは、同じ文字を探す場合、REPを抜けたときのDIは １つずれて、同じ値の次の値を指している。逆に違う文字を探す場合も、REPを 抜けたときのDIは１つずれていることに注意してください。


ストリング命令

7-7.SCAS?

CMPSB

    動作：[DS:SI]と[ES:DI]とを比較する
          DF=1の時は、SI,DIを1減らす。
          DF=0の時は、SI,DIを1増やす。
    影響を受けるフラグ：OF,SF,ZGF,AF,PF,CFが結果を反映する

CMPSW

    動作：[DS:SI]と[ES:DI]とを比較する
          DF=1の時は、SI,DIを2減らす。
          DF=0の時は、SI,DIを2増やす。
    影響を受けるフラグ：OF,SF,ZGF,AF,PF,CFが結果を反映する

CMPSD	(80386以上)

    動作：[DS:SI]と[ES:DI]とを比較する
          DF=1の時は、SI,DIを4減らす。
          DF=0の時は、SI,DIを4増やす。
    影響を受けるフラグ：OF,SF,ZGF,AF,PF,CFが結果を反映する
    上のニーモニック表の通り、「CMPS?」命令は[DS:SI]の内容と[ES:DI]の内容を比較する。 SI,DIが増えるか減るかは、「MOSV?」と同様、DF(フラグ)によって 変わる。

    この命令は主に、次の用途に使われる。
CXにデータ領域のバイト数を入れ「REPNE(REPNZ)」と組み合わせ、データの内容が同じであるかどうかを調べる
    「SCAS?」と同様、REPE(REPZ)/REPNE(REPNZ)を抜けたときのSI,DIはひとつずれていることに 注意してください。


ストリング命令

7-9.練習問題

問題
4-9で作った１行入力サブルーチンを用いて、文字列２つを 入力する。２回目の文字列が１回目の文字列の中に何回出現するかどうかを数えて、 その出現回数を表示するプログラムを作れ。


ストリング命令

7-9.練習問題

問題
4-9で作った１行入力サブルーチンを用いて、文字列２つを 入力する。２回目の文字列が１回目の文字列の中に何回出現するかどうかを数えて、 その出現回数を表示するプログラムを作れ。
考え方
  1. １回目の文字列を入力
  2. 何も入力されていなかったらエラーメッセージを表示して終了
  3. ２回目の文字列を入力
  4. 何も入力されていなかったらエラーメッセージを表示して終了
  5. １回目の文字列の方が２回目の文字列より短かったらエラーメッセージを表示して終了
  6. DS:SIは１回目の文字列の先頭を指す
  7. ES:DIは２回目の文字列の先頭を指す
  8. CMPSBで比較する
  9. 同じであれば個数を１増やす
  10. SIを1増やして、7に戻る。１回目の文字列の最後まできたら終了
  11. 個数を表示して終了

解答
START:
    push	ds
    pop	es
    cld
    mov	bx,offset DAT1
    mov	cx,70
    call	LINEINP
    or	ax,ax		;1st data ==0 -> ERR
    jz	ERR2
    mov	LEN1,ax

    mov	bx,offset DAT2
    call	LINEINP
    or	ax,ax
    jz	ERR2		;2nd data == 0 -> ERR

    mov	LEN2,ax
    cmp	LEN1,ax
    jae	SCAN		;2nd data > 1st data ERR

ERR1:	mov	dx,offset ERRMSG1
    jmp	short ERR_END
ERR2:	mov	dx,offset ERRMSG2
;	jmp	short ERR_END
ERR_END:
    mov	ah,9
    int	21h		;エラーメッセージ表示
    mov	ax,4cffh
    int	21h

SCAN:	mov	si,offset DAT1-1
    mov	ax,LEN1
    add	ax,offset DAT1	;ax<= END address
SCAN2:	inc	si
SCAN3:	cmp	si,ax
    jae	SCAN_END
    mov	di,offset DAT2
    mov	cx,LEN2
    push	si
    repe	cmpsb		;ds:si = DAT1
                ;es:di = DAT2
    pop	si
    jnz	SCAN2
    add	si,LEN2
    inc	KOSU
    jmp	short SCAN3
SCAN_END:

    mov	ax,KOSU
    call	DISPN

_END:	call	KAIGYOU
    mov	ax,KOSU
    mov	ah,4ch
    int	21h

;+------------------------
;|１行入力
;|最後には 0Dh が入る
;+------------------------
;|in  bx <= buffer(offset)
;|    cx <= 最大文字数
;+------------------------
;|out ax <= 入力文字数
;|          (改行なし)
;+------------------------
;|break ; ax
;+------------------------
LINEINP	proc
    push	dx
    push	di
    inc	cx
    mov	di,0
INPUT:	mov	ah,8
    int	21h

    cmp	al,8		;BSか？
    jz	_BS

    mov	[bx+di],al

    cmp	al,0dh		;リターンキーか？
    jz	_RET

    cmp	di,cx		;文字数制限大丈夫？
    jz	OVER
    inc	di

DISPC:	mov	dl,al
    mov	ah,2
    int	21h		;disp char

    jmp	short INPUT

OVER:
    mov	[di+bx-1],al	;入力可能文字数を超えていた場合の処理
    push	ax
    mov	ah,2
    mov	dl,8
    int	21h		;disp BS
    pop	ax
    jmp	short DISPC

_BS:	or	di,di
    jz	INPUT		;何も入力されていなかったらBS無効

    mov	ah,2
    mov	dl,8
    int	21h		;disp BS
    mov	ah,2
    mov	dl,' '
    int	21h		;disp SP
    mov	ah,2
    mov	dl,8		;disp BS
    int	21h
    dec	di
    jmp	short INPUT

_RET:
    mov	ah,2
    mov	dl,0dh
    int	21h
    mov	ah,2
    mov	dl,0ah
    int	21h		;改行

    mov	ax,di
    pop	di
    pop	dx
    dec	cx
    ret

;+--------------
;|数字を表示する
;+--------------
;|IN  cx <= 数字
;+--------------
;|OUT none
;+--------------
;|break none
;+--------------
DISPN	proc
    pusha
    mov	cx,5
DISPN_10:
    xor	dx,dx
    mov	bx,10
    div	bx
    add	dl,30h
    push	dx
    loop	DISPN_10

    mov	cx,5
DISPN_20:
    pop	dx
    mov	ah,6
    int	21h
    loop	DISPN_20
    popa
    ret
;+------------
;|改行する
;+------------
;;break ; none
;+------------
KAIGYOU	proc
    push	ax
    push	dx
    mov	ah,2
    mov	dl,0dh
    int	21h
    mov	dl,0ah
    int	21h
    pop	dx
    pop	ax
    ret

KOSU	dw	0
ERRMSG1	db	'2nd string is longer than 1st one!',0dh,0ah,24h
ERRMSG2	db	'You inputed nothing!',0dh,0ah,24h
DAT1	db	71 dup(0)
DAT2	db	71 dup(0)
LEN1	dw	?
LEN2	dw	?

    end


入出力

8-1.概要

    入出力はI/O(Input / Output)と略される。コンピュータのI/Oはもう少し意味が拡張されており、 キーボード、プリンター、ディスプレイなど「うん、入出力機器だな」とわかるものから、 タイマーなど、「えっ、入出力機器なの？」というものまで含まれ、キーボードでも キーボードを制御する半導体などもI/Oと言う。

    今半導体という言葉を出したが、ほとんどの装置はその装置を制御する半導体があるから こそ使える。下図のようにその制御する半導体はメモリーと同じように、制御線、アドレスバス、 データバスからなる。

    ということは、CPUから見れば、メモリーと全く同じなのである。ただしコンピュータによっては その扱い方が異なり、その方法は２種類ある。

Memory Mapped I/O
    主に、モトローラ系CPU、日立系CPU、NEC系CPUで採用されている。

    例えばアドレスバスが16bitのCPUでは65536バイトのメモリー空間を使用できる。 そのうち少しをこのI/O用のデバイス制御の空間を設ける方法である。

    65536バイトのうち、上位256バイト分をこのデバイス制御に割り当てると、その256 バイトに「MOV」命令のような転送命令でデータを書き込むと、メモリーではなくて、 デバイス制御装置にデータが渡される。逆にその256バイトからデータを読もうとすると、 メモリーからではなくて、デバイス制御装置の状態が読みとれる方法である。

    この方法では、アクセス方法は非常に簡単だが、メモリー空間が少なくなること、 プログラムが暴走した際に、偶然にもディスクコントローラーにデータを送ってしまうと、 フロッピーが壊れたり、何が起こるか全く予想がつかないなどの欠点がある。

I/O Mapped I/O
    主に、インテル系CPU、NEC系CPUに採用されている。

    x86系CPUで使われている方法で、例えばx86系CPUではメモリー空間のほかに、 64Kバイト(65536バイト)分の入出力専用の空間を設けている。入出力に関わるデータ転送 などは、この専用の空間を用いてアクセスする。

    従って、メモリー専用の入出力命令「MOV」ではなくて、専用の命令を用いなければならない。 それがこの章で解説する「IN」、「OUT」、 「INS」、「OUTS」である。

    この方法では、メモリー空間とは別の専用空間を用いるので、メモリー空間が少なくなること がなく、プログラムが暴走してもフロッピーが壊れるなど予期せぬアクシデントが少ない。


入出力

8-2.IN命令

    IN命令は入出力デバイスの状態を読み出す命令である。
IN	DEST,SRC

    動作：SRCのI/Oポートに接続されているデバイスの状態を読み出す
    影響を受けるフラグ：なし
    DEST：EAX(80386以上) / AX / AL
    SRC ：即値(8bit)、DX
    ポートのアドレス0000h～00FFhにアクセスするときは、SRCに既値が利用できる。 すべてのポート(0000h～FFFFh)にアクセスするにはDXレジスタを使わなくてはならない。 DESTはポートのサイズ(プログラマーが調べなくてはならない)によって、レジスタのサイズを 決める。

    コンピュータの画面は左上から横に１行ずつ描画し、最後に右下に至る。 そこまで来るとまた左上に戻る。これを繰り返すことで、画面が私たちに見える。 PC-9801(PC-9821)シリーズでは右下を書き終わり、左上に戻る瞬間に、A0hポートの データ(1バイト)の6bit目が1になる。これを利用して、右下に来るまで待つプログラムを 作ることができる。
WAIT_SYNC:
    in	al,0a0h
    test	al,00100000b
    jne	WAIT_SYNC
なぜこんなことが必要かというと、VRAM(表示するグラフィックを格納するメモリー)に 書くタイミングを計らないと、見た目がちらついてしまうため。今はそんなことは気にしなくても 大丈夫。あくまでも昔の話・・・。


入出力

8-3.OUT命令

    OUT命令は入出力デバイスにコマンドを送る命令である
OUT	DEST,SRC

    動作：DESTのI/Oポートに接続されているデバイスにコマンドを送る
    影響を受けるフラグ：なし
    SRC ：EAX(80386以上) / AX / AL
    DEST：即値(8bit)、DX
    ポートのアドレス0000h～00FFhにアクセスするときは、DESTに既値が利用できる。 すべてのポート(0000h～FFFFh)にアクセスするにはDXレジスタを使わなくてはならない。 SRCはポートのサイズ(プログラマーが調べなくてはならない)によって、レジスタのサイズを 決める。このサイズを間違えると、関係ない隣のポートにもコマンドを送ることになり、 何が起こるかわからない。

    PC-9801(PC-9821)シリーズでは、グラフィック画面とテキスト画面という２つの VRAMを持っている。それぞれ、表示/非表示を行うことができる。下のプログラムは テキスト画面を非表示にするものである。
    mov	al,0ch
    out	062h,al		;TEXT off


入出力

8-4.INS,OUTS命令

    7章で、ストリング命令というのを説明した。80186以上のCPUでは IN、OUT命令もストリング命令として、「REP」と組み合わせることができる。
INSB

    動作：DXのポートから1バイト読み出して[ES:DI]に転送し、
          DF=1の時は、DIを1減らす。
          DF=0の時は、DIを1増やす。
    影響を受けるフラグ：なし

INSW

    動作：DXのポートから2バイト読み出して[ES:DI]に転送し、
          DF=1の時は、DIを2減らす。
          DF=0の時は、DIを2増やす。
    影響を受けるフラグ：なし

INSD	(80386以上)

    動作：DXのポートから4バイト読み出して[ES:DI]に転送し、
          DF=1の時は、DIを4減らす。
          DF=0の時は、DIを4増やす。
    影響を受けるフラグ：なし

OUTSB

    動作：DXのポートへ[DS:SI]の内容1バイトを書き出して、
          DF=1の時は、DIを1減らす。
          DF=0の時は、DIを1増やす。
    影響を受けるフラグ：なし

OUTSW

    動作：DXのポートへ[DS:SI]の内容2バイトを書き出して、
          DF=1の時は、DIを2減らす。
          DF=0の時は、DIを2増やす。
    影響を受けるフラグ：なし

OUTSD	(80386以上)

    動作：DXのポートへ[DS:SI]の内容4バイトを書き出して、
          DF=1の時は、DIを4減らす。
          DF=0の時は、DIを4増やす。
    影響を受けるフラグ：なし


その他

9-1.LEA

    実行アドレスを計算します。
LEA	DEST,SRC

    動作：SRCの実行アドレスを計算し、DESTに代入
    影響を受けるフラグ：なし
    SRC1：メモリー
    DEST：16bitもしくは32bitレジスタ
    これは機能を解説するのはわかりにくいので、例を紹介します。
lea	ax,[si+bx]	; AX←SI+BX
lea	ax,[si+bx+5]	; AX←SI+BX+5
lea	edx,[eax+eax×2]	;EAX←EAX×3(80386以上)
[ ]で囲まれているので、基本的にはSRCは下記のものしか書けません。
[BX]、[SI]、[DI」
[BX+SI]、[BX+DI]、[BP+SI]、[BP+DI]
[BX+disp]、[SI+disp]、[DI+disp」
[BX+SI+disp]、[BX+DI+disp]、[BP+SI+disp]、[BP+DI+disp]

    ※disp : 即値
    しかし、80386以上では、EAX,EBX,ECX,EDXなどすべての32bitレジスタで 間接アドレス法が使えます。しかもかけ算(ただし、1,2,4,8倍のみ)が書けるのです。 (16bitレジスタは使用できない)
[ 32bitレジスタ ＋ 32bitレジスタ × 1 or 2 or 4 or 8 ＋ 既値 ]
    したがって、簡単な計算ができるわけです。

    また、すべてのCPUで「mov　bx,offset DATA」を「lea　bx,DATA」と 書くこともできます（一般的でないが）。


その他

9-2.XCHG

XCHG	DEST1,DEST2

    動作：DEST1とDEST2を交換する
    影響を受けるフラグ：なし
    DEST1：レジスタ、メモリー
    DEST2 ：レジスタ、メモリー（ただしメモリー、メモリーの組み合わせは除く）
    上のニーモニックの通り、２つのオペランドを交換します。セグメントレジスタは 使用できません。


その他

9-3.NOP

NOP

    動作：IP←IP + 1
    影響を受けるフラグ：なし
    何もしません。「XCHG AX,AX」と等価です。

用途
  1. デバッグ中に不要な命令を見つけた場合に、とりあえず「NOP」にして、不要な命令をとばす。
  2. 例えばシューティングゲームの当たり判定サブルーチンなど、テストラン(試し実行)の時は、 飛ばしたいサブルーチンの先頭に置いておき、テストラン時はこの「NOP」を「RET」に書き換えたり する。すなわち、場所を確保するのに使う。
  3. ２番目の目的を発展させ、プログラムが自分自身を書き換えるための、場所の確保。
  4. いけない人が、不正使用防止のためのルーチンを飛ばすため、そのルーチンを「NOP」に する
      などの使い方がある。


割り込み

10-1.割り込みとは

    割り込みはよく「電話」に例えられる。何か仕事をしていたとする。そこに電話が 鳴ると、仕事の手を休め電話に出る。これが割り込みの概念である。何かの要因によって 今やっている仕事をやめ、急遽別の仕事をすることである。

    コンピュータにとっても割り込みは重要である。例えばキーボード。人間がキーボードを いつ押すかコンピュータにはわからない。かといってコンピュータがずっとキーボードを 監視していたのではほかの処理ができない。

    Mac OSはフロッピーやCD-ROMなどを入れると、画面にそのアイコンが出る。しかし いつフロッピーなどが入ってくるのかわからないのに、ずっとディスクを監視していたら、 やはりほかの仕事はできない。

    このように何かイベントが起こったときにCPUにその旨を知らせ、そのイベントに あった処理をして、その処理が終わると、今までやっていた仕事を続ける。これを 「割り込み」という。

    さらに割り込みには「ハードウェア割り込み」「 ソフトウェア割り込み」がある。


割り込み

10-2.ソフトウェア割り込み

    ソフトウェア割り込みとは、プログラマーが故意もしくはミスにより発生させる割り込みである。 ソフトウェア割り込みは以下の３種類に分類できる。
  • トラップ・・・
  • フォルト・・・
  • アボート・・・
プログラマーが故意に呼び出す割り込み(INTなど)
プロテクトモードでの保護違反など
0での除算など、処理を続行できないエラー

トラップ
    今まで何回も出てきた「INT」命令を用いてプログラムから強制的に割り込みを発生させる ことを言う。「INT　21h」のようにOSが供給しているサービスや、常駐プログラムが行う サービス、BIOSというハードウェアを操作するためのROMが供給するサービスなどを、 あたかもサブルーチンのように呼び出すかのように使うことができる。

フォルト
    プロテクトモードでは、OSがプログラムに対して、使えるメモリー領域を制限する。 この制限を越えてメモリーを使おうとするとエラーが出る。この制限を越えたメモリー を使用しようとした時にこのフォルトが起こり、処理がプログラムから強制的に 「エラー表示をして、そのプログラムを終了させる」というOSの処理に移行する。

    下図のような画面を見たことがないだろうか？「ページ違反」と書いてあるということは 許可されていないメモリーを読もうとした証拠である。

アボート
    「わり算」のところで少しふれたが、0で割ったり、商が大きいと 「例外」が発生すると書いた。それがこのアボートである。

    下図の画面では「除算オーバーフローエラー」と書いてある。Windowsから「コマンドプロンプト」を 呼び出し、その中で0で割るプログラムを実行した場合の例である。本当のMS-DOSからこのプログラムを 実行させると、MS-DOSに処理が移行し、何も続きができなくなり、リセットするしか方法が なくなる。

    次ページのハードウェア割り込みも含めて、すべての割り込み というのは、何らかの理由により、現在行っている処理をやめ、ほかの処理を行うルーチンに 飛ぶことである。どこに、どのように飛ぶのかは後述する。


割り込み

10-3.ハードウェア割り込み

    コンピュータの周辺機器はたくさんあり、タイマーのように周辺機器とはユーザーが 認識していないものまで含まれている。そしてそれら周辺機器に操作が加えられたり、 周辺機器の操作が完了したり、異常が発生した場合に割り込みが発生する。しかし CPUには割り込みを受ける線は１本しかない。そこで割り込みコントローラーという ICが活躍する。

    この割り込みコントローラーはPC-9801(9821)シリーズ、DOS/Vパソコンともに、 8259(もしくはその互換チップ)というチップで、これは8台の機器がつなげる。 しかし8台では少ないので、上図のように8259を２つつなげて、15台の機器を管理している。 キーボードやタイマーなどは1つめの8259(マスターコントローラーという)につながっており、 マウスなどは2つめの8259(スレーブコントローラーという)につながっている。 (どこにつながっていてもプログラマーには関係ないんだけど)

    そして、各機器から割り込み要求があると、8259がCPUに「割り込みだよ！」と 知らせる。するとCPUは今実行している命令が終わると(ある処理が終わったらではなくて 命令が終わると)、8259に「誰から？」と訪ね答えを聞く。実際は割り込み番号を聞く。

    電話中に他の電話がかかってくると出られないのと一緒で、CPUも割り込み処理中は 他の割り込みを受け付けることができない。そこで8259に「今割り込み処理してるからね」 と教える。

    受け取った割り込み番号ごとに、実行すべき割り込み処理のプログラムが格納されている 場所がメモリー上に記録されており、そのアドレスを調べて割り込み処理をする。割り込み 処理が終わると8259に「割り込み処理がやっと終わったよ」と知らせる。もし、割り込み 処理中に他の割り込みがあると、8259はそれを覚えておいて、この時点でCPUに「また割り込み があったよ」と知らせて、CPUは割り込みをまた行う。もし割り込み処理中に新たな割り込みが なければ、それまでやっていた処理の続きを行う。

    なお、同時に複数の割り込みがあった場合は8259が優先度を決めて(8259につながっている 順番)CPUに知らせてくれる。


割り込み

10-4.割り込みベクター

    では、割り込み番号からどのように割り込み処理のルーチンに飛ぶのだろうか？

    その前に、もう一度おさらいをする。ハードウェア割り込みの場合は割り込みが発生すると 割り込みコントローラーから割り込み番号を受け取る。プログラマーが「INT」命令などで、 行うトラップは、「INT　21h」などのように、割り込み番号はプログラムとして書かれている。 除算エラーなどのアボートやフォルトの場合はROMに書かれている割り込み番号を受け取る。

    そしてその割り込み番号から実際の処理に移行するには「割り込みベクター」と呼ばれる メモリーを参照してそこにジャンプする。80186以下のCPUや80286以上のリアルモードでは、 00000h～00400hに割り込みベクターがセットされている。CPUの決まりであり、OSの決まりでは ない。したがってMS-DOS、N88-BASIC、その他のOS(Windowsなんかはプロテクトモードなので、 違う)でも同じ領域にセットされる。

    左図のように、割り込み番号0の処理は、アドレス00000hにそのオフセットアドレス が、00002hにセグメントアドレスが入っている。割り込み番号0の処理は、アドレス00004hに そのオフセットアドレスが、00006hにセグメントアドレスが入っている。これが256個分、 格納されている。したがって例えば割り込み番号0が発生すると、CSに[00002h]が IPに[00000h]の内容が格納され、そのアドレスにジャンプする。

    次に、割り込み番号0が発生した際の処理の流れを、詳しく説明する。
  • 割り込みが発生する
  • トラップ(INT　??での呼び出し)以外の場合は、割り込み許可フラグ(IF)を0にし、 他の割り込みを保留する
  • フラグレジスタ、CSレジスタ、IPレジスタをこの順番にプッシュする
  • 割り込みベクターを参照し、その処理ルーチンに移行する
  • その割り込み処理を実行する
  • その割り込み処理の最後には、割り込みコントローラーに対して、割り込み処理の 終了を通知するプログラムがあるので、それを実行する(トラップ以外)
  • 割り込み処理ルーチンの終わりを告げる「IRET」が実行される
  • 「IRET」が実行されると先ほどプッシュしたCS、IP、フラグをポップする
  • CS、IPがポップされると言うことは、もとの処理に戻るという意味で、もとの処理を続行する

    このような流れで割り込み処理を実行する。


割り込み

10-5.割り込み処理の作り方

    では、実際に割り込み処理を作ってみる。その前に作るためにはどうすればよいのかを説明する。

割り込み処理内
  • 「CLI」命令を用いて割り込み許可フラグを0にする
  • 処理をする
  • 割り込みコントローラーに割り込み処理を終えたことを通知する
  • 「STI」命令を用いて割り込み許可フラグを1にする
  • IRETで戻る
  • もし、もとの処理も実行したい場合は、割り込みコントローラーに割り込み処理を 終えたことを通知する前に、もと処理へfar jumpする
割り込み処理を設定する場合
  • MS-DOSのシステムコールに割り込みベクターを設定する方法があるのでそれを使用する。 ただし、もとのアドレスを保管するのを忘れずに
  • MS-DOS以外のOSでは、自分で割り込みベクターを書き換えなくてはならない。
    • 「CLI」命令を用いて割り込み許可フラグを0にする（割り込みベクターを書き換えている 途中にその割り込みが入ると、予期せぬところに処理が行ってしまうから）
    • 割り込みベクターの位置を計算し、現在のアドレスを保管する。
    • 新しい処理のアドレスを書き込む
    • 「STI」命令を用いて割り込み許可フラグを1にする

割り込みはいつ、いかなる状態で呼ばれるかわからないので、以下に注意する必要がある
  • レジスタの値はわからない(セグメントを含め)
  • レジスタを破壊してはいけない(元に戻ったときに処理を続行できない)
  • あまりPUSH,POPなどを多用しない(スタック領域の大きさがわからないので)
  • 処理はできるだけ速く終わらせる。(もとの処理が何の処理かわからないので速くその処理に戻さなくてはいけない)
  • 割り込み内容にもよるが、OSのサービス(システムコール)を利用できない (キーボードからの入力をしようと思っても、キーボードの割り込みが起こらない)

・割り込みベクターの設定

AH=25h
AL=割り込み番号
DS=新しい割り込み処理のセグメント
DX=新しい割り込み処理のオフセット
をセットし、「INT　21h」を実行する

・割り込みベクターの取得

AH=35h
AL=割り込み番号
をセットし、「INT　21h」を実行する
ES=現在の割り込み処理のセグメント
BX=現在の割り込み処理のオフセット


割り込み

トップ-> アセンブラ入門：10章.割り込み-> 10-5.割り込み処理の作り方

    では、除算エラーのエラーメッセージを変えてみよう。Windows98のコマンドプロンプトから 0で割るプログラムを実行すると下のようになる。

    このメッセージを変えてみるプログラム２つを下記に示した。上は、MS-DOSのシステムコール を用いて割り込みベクタの取得、設定を行っている。下は、システムコールを使わずに、 割り込みベクタの取得、設定を行っている。
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	ax,3500h
    int	21h		;割り込みベクタ取得
    mov	VECT0_SEG,es
    mov	VECT0_OFF,bx	;割り込みベクタ保管

    push	cs
    pop	ds		;DS <= CS
    mov	dx,offset VECT0
    mov	ax,2500h
    int	21h		;割り込みベクタ設定

    xor	ax,ax
    div	al		;0で割ってみる

    mov	ah,9
    mov	dx,offset MSG
    int	21h		;メッセージ表示
    mov	ax,2500h
    mov	dx,VECT0_OFF
    mov	ds,VECT0_SEG
    int	21h		;割り込みベクタを戻す

    mov	ax,4c00h
    int	21h		;終了
;-------------------------------------
VECT0:
    cli			;割り込み禁止
;	push	ax		;※注参照
    push	dx		;レジスタ保管
    push	ds		;レジスタ保管

    push	cs
    pop	ds
    mov	ah,9
    mov	dx,offset INT_MSG
    int	21h		;表示

    pop	ds
    pop	dx
    mov	al,20h
    out	20h,al		;割り込みコントローラーに終わりを告げる
;	pop	ax		;※注参照
    sti			;割り込みを許可する
    iret			;終了

VECT0_SEG	dw	?
VECT0_OFF	dw	?
MSG	db	'ゼロで割っちゃった',0dh,0ah,24h
INT_MSG	db	'割り込み処理中です',0dh,0ah,24h

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

;※	本当ならすべてのレジスタを保管しなくてはならない。
;	普通の割り込みでは割り込みが発生した命令の次の命令のIPが保存されるが、
;	除算エラーの場合、原因となった命令のIPが保存される(80186以上)。
;	したがって割り込み処理が終わると、また除算命令に戻ってしまう。
;	このため、例外処理を無限に繰り返してしまうので、終了しなくなってしまう。
;	AXレジスタを保管しないことで、AXレジスタの内容を変え、エラーが
;	繰り返されないようにするため、わざとAXレジスタは保管しなかった。

CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    xor	di,di

    xor	ax,ax
    mov	ds,ax
    mov	bx,[di+0]
    mov	es,[di+2]
    mov	VECT0_SEG,es
    mov	VECT0_OFF,bx	;割り込みベクタ保管

    cli			;割り込み禁止
    mov	[di+0],offset VECT0
    mov	[di+2],cs	;割り込みベクタ設定
    sti			;割り込み許可
    push	cs
    pop	ds

    xor	ax,ax
    div	al		;0で割ってみる

    mov	ah,9
    mov	dx,offset MSG
    int	21h		;メッセージ表示

    mov	dx,VECT0_OFF
    mov	ds,VECT0_SEG
    xor	ax,ax
    mov	ds,ax
    mov	[di+0],dx
    mov	[di+2],ds	;割り込みベクタを戻す

    mov	ax,4c00h
    int	21h		;終了
;-------------------------------------
VECT0:
    cli			;割り込み禁止
;	push	ax		;※注参照
    push	dx		;レジスタ保管
    push	ds		;レジスタ保管

    push	cs
    pop	ds
    mov	ah,9
    mov	dx,offset INT_MSG
    int	21h		;表示

    pop	ds
    pop	dx
    mov	al,20h
    out	20h,al		;割り込みコントローラーに終わりを告げる
;	pop	ax		;※注参照
    sti			;割り込みを許可する
    iret			;終了

VECT0_SEG	dw	?
VECT0_OFF	dw	?
MSG	db	'ゼロで割っちゃった',0dh,0ah,24h
INT_MSG	db	'割り込み処理中です',0dh,0ah,24h

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

;※	本当ならすべてのレジスタを保管しなくてはならない。
;	普通の割り込みでは割り込みが発生した命令の次の命令のIPが保存されるが、
;	除算エラーの場合、原因となった命令のIPが保存される(80186以上)。
;	したがって割り込み処理が終わると、また除算命令に戻ってしまう。
;	このため、例外処理を無限に繰り返してしまうので、終了しなくなってしまう。
;	AXレジスタを保管しないことで、AXレジスタの内容を変え、エラーが
;	繰り返されないようにするため、わざとAXレジスタは保管しなかった。
← 前ページへ　:　 トップへ　:　 次ページへ →


割り込み

10-6.練習問題

問題
約0.99秒ごとに、'*'を１つずつ表示するプログラムを作れ。ただし、リターンキーが押されたら 終了する。
ヒント
  • DOS/V機では約55msに1回、割り込み番号8番がタイマー割り込みとして呼び出される。 約0.99秒ということは18回呼び出されるたびに'*'を表示すればよい。
  • キーボードからの入力には、CTRL+Cを押されたら終了されては困る。何か押されるまで 待ってもらっては困る。この場合はAH=6、DL=FFhの状態で「INT　21h」を実行する。もし 何も入力されていなければAL=0、入力されればALに入力文字が返ってくる。


割り込み

10-6.練習問題

問題
約0.99秒ごとに、'*'を１つずつ表示するプログラムを作れ。ただし、リターンキーが押されたら 終了する。
ヒント
  • DOS/V機では約55msに1回、割り込み番号8番がタイマー割り込みとして呼び出される。 約0.99秒ということは18回呼び出されるたびに'*'を表示すればよい。
  • キーボードからの入力には、CTRL+Cを押されたら終了されては困る。何か押されるまで 待ってもらっては困る。この場合はAH=6、DL=FFhの状態で「INT　21h」を実行する。もし 何も入力されていなければAL=0、入力されればALに入力文字が返ってくる。

CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	ax,3508h
    int	21h		;割り込みベクタ取得
    mov	VECT8_SEG,es
    mov	VECT8_OFF,bx	;割り込みベクタ保管

    push	cs
    pop	ds		;DS <= CS
    mov	dx,offset VECT8
    mov	ax,2508h
    int	21h		;割り込みベクタ設定

MAIN:
    mov	ah,6
    mov	dl,0ffh
    int	21h		;入力
    cmp	al,0dh
    jz	QUIT		;リターンキーだったら終了
    cmp	COUNT,18
    jnz	MAIN
    mov	ah,6
    mov	dl,'*'
    int	21h
    mov	COUNT,0
    jmp	short MAIN

QUIT:
    mov	ax,2508h
    mov	dx,VECT8_OFF
    mov	ds,VECT8_SEG
    int	21h		;割り込みベクタを戻す

    mov	ax,4c00h
    int	21h		;終了
;-------------------------------------
VECT8:
    cli			;割り込み禁止
    push	ax		;レジスタ保管

    inc	cs:COUNT

    mov	al,20h
    out	20h,al		;割り込みコントローラーに終わりを告げる
    pop	ax		;
    sti			;割り込みを許可する
    iret			;終了

VECT8_SEG	dw	?
VECT8_OFF	dw	?
COUNT		db	0

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです


アセンブラ入門

    アセンブラ入門へようこそ。

    今、アセンブラのコンパイラって結構手に入らないみたいですね。そこでフリーでダウンロードできる サイトを紹介します。

  • NOWSMART ASSEMBLER
  • 多摩ソフトウェア LASM Ver2.32 体験版
    また、私がよく読むアセンブラの本を紹介します。今手に入るかどうかはわかりませんが。。。
出版社 | タイトル | ISBN | 備考
啓学出版 | 8086マシン語秘伝の書 | 4-7665-1079-8 | 効率の良いプログラミングテクニックのチップ集
技術評論社 8086マクロアセンブラ入門 4-7741-1442-1 なぜ今頃？と言いたくなるような2002年3月出版のアセンブラの本
ちなみに私は読んだ事はありません。
インテル インテル・アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル、
上巻：基本アーキテクチャ (日本語)
インテル インテル・アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル、
中巻：基本アーキテクチャ (日本語)
インテル インテル・アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル、
下巻：基本アーキテクチャ (日本語)
ボーランド Turbo Assembler 3.0 Quick Reference Guide
アセンブラ命令のリファレンス集
本の検索・ご購入は「 コンピュータの本・専門店」で。

←目次から選んでください

・ トップページに戻る


レジスタの型変換(キャスト)

11-1.レジスタの型変換(キャスト)とは

    高級言語では1バイトの変数を2バイト(short)や4バイト(long)などに拡張することができる。 これを型変換(キャスト)という。アセンブラの世界でも、レジスタに限って型変換(キャスト)を 行うことができる。変数は実メモリーに容量を確保するために拡張できない。アセンブラででは、 例えばALレジスタをAXレジスタに拡張する場合、符号なし整数ではレジスタの上位AHを0にして、 以降の計算をALではなく、AXレジスタを使うだけでできる。

    mov	al,0F0h
    add	al, 50h
;	この場合、桁が足りなくなってしまう。そこで次のようにする

    mov	al,0F0h
    mov	ah,0
    add	ax,50h
;	この例では　ah=0　にし、以降の計算(ADD命令)をAXで行うことで、型変換している

    しかし、符号付き整数の場合同じようなことをやると、負数の場合値がおかしくなる。

    mov	al,-1
    add	ah, 0
;	AX = +FFhになってしまい、AX=-1ではない

    そこで、符号付き整数の場合は次の命令を使用する。
CBW
    動作：ALレジスタを符号付き整数としてAXに型変換する(8bit→16bit)
          8086以上のCPUで使用可

CWD
    動作：AXレジスタを符号付き整数としてDX:AXに型変換する(16bit→32bit)
          8086以上のCPUで使用可

CWDE
    動作：AXレジスタを符号付き整数としてEAXに型変換する(16bit→32bit)
          80386以上のCPUで使用可

CDQ
    動作：EAXレジスタを符号付き整数としてEDX:EAXに型変換する(32bit→64bit)
          80386以上のCPUで使用可

使用例

    mov	al,-5
    cbw
;	この状態で、AX=-5となる。


卒業試験

12-1.卒業試験

    ここでは卒業試験と題しまして、Windowsのグラフィックツールを作ってもらいます。 コンパイラはMicrosoftのVisual C++、Ver6.0です。すべてアセンブラで作るのは無理なので、 C++でビットマップのロード、セーブ、表示は作ってあります。これに、モザイク、エンボス、 ディザ拡散による減色、左右反転、上下反転、ネガポジ反転といった機能をアセンブラで 作ってもらいます。

    まず、始める前に雛形をダウンロードしてください。指定のコンパイラが無い場合は 諦めてください。(^_^;)

    次ページから、ビットマップの形式、各機能のアルゴリズム等を解説します。


卒業試験

12-2.画像データの格納形式

    BMPファイルはビットマップの略です。各点がビットマップにしたがって格納されています。 色が最高で何色表現できるか、Windows用かOS/2用かなどによりビットマップの種類が異なり、 同じBMPファイルでも何種類かのフォーマットがあります。

    Windows用のBMPファイルは、DIBという形式になっています。DIBには何色格納できるか などにより、いくつかの形式があるのでコンピュータから見ると、ちょっと扱いにくいフォーマット です。そこでコンピュータの中ではDDBという形式に変換して、格納しています。

    しかしDDBは人間にとっては扱いにくいので、本ソフトではDIBすなわちBMPファイルの 形式のデータを操作(モザイクなど)することにします。そして画面に表示するときに、DDB に変換します。(DDBへの変換等はもちろん雛形に組み込まれています。)

    コンピュータの画像は点の集合ですが、1点(1ドット、1ピクセルなどと呼ぶ)を どのような情報で表しているかというと、光の三原色(赤、緑、青)です。他に YUV(YCrCbとも言い、テレビやビデオなどに使われている)やHSVなどの情報に分ける こともあります。

    一般にフルカラーと呼ばれる画像は1ピクセルを赤、緑、青それぞれ1バイト(256階調) の合計3バイトで管理しています。これが一番操作しやすいので、今回のアプリケーションは 24bitフルカラーのみ対応のアプリケーションです。

    では次ページから、メモリーとの関連を説明します。


卒業試験

12-2.画像データの格納形式

    ここでは簡単な例として、高さ3ピクセル、幅2ピクセルの画像を考える。(右図参照)

    このDIBが例えば、1000h番地から格納されているとすると、下表のように格納されている。
番地 | 1000h | 1001h | 1002h | 1003h | 1004h | 1005h | 1006h | 1007h
内容 | R1 | G1 | B1 | R2 | G2 | B2 | ゴミ | ゴミ
番地 | 1008h | 1009h | 100Ah | 100Bh | 100Ch | 100Dh | 100Eh | 100Fh
内容 | R3 | G3 | B3 | R4 | G4 | B4 | ゴミ | ゴミ
番地 | 1010h | 1011h | 1012h | 1013h | 1014h | 1015h | 1016h | 1017h
内容 | R5 | G5 | B5 | R6 | G6 | B6 | ゴミ | ゴミ
    DIBでは1行は必ず4の倍数バイトでなければならないという規格なので、幅が2ピクセルの場合は 6バイトになるが、4の倍数にするため、2バイトのゴミが入って、次の行のデータになる。この ゴミは何が入っていても構わないが、変更しないのが無難である。

    当然ながら、1018h番地以降は、このプログラムの管理外なので、さわってはいけない。 触るどころか、覗くだけでもいけない。触ったり、覗いたりするとWindowsに怒られて、 プログラムが強制終了されてしまう。ちなみにこのように他のプログラムが使っている メモリーを覗いたりする事ができない、すなわち保護(プロテクト)されている環境を プロテクトモードといい、MS-DOSのように他のプログラム領域を覗いたり触ったり できる環境をリアルモードという。


卒業試験

12-3.インラインアセンブラ

    今回のようにC++などの他の言語の中の一部をアセンブラで書くような機能を インラインアセンブラと言う。C++(C)言語でのインラインアセンブラでの注意事項を あげる。

サブルーチンは作れない
    というか、C++(C)言語および、コンパイラのことを熟知していないと、作れない。
    アセンブラの部分の中のみで使用する分には問題ない。したがって、各機能共通の サブルーチンを作ることはできない(今回はしない)。もしそのようなサブルーチンを 作る場合は、コピー＆ペーストで、毎回作るしかない。

レジスタの制限
    レジスタは、EBP(BP)、ESP(SP)は使用しない。これを勝手に書き換えたりすると C++(C)の部分に戻ることができないで、暴走する。
    PUSH、POP、CALL、RETなどで、ESPを使用する分には問題ないが、C++(C)部分に 戻るときは、前のESPを示しているようにしないといけない。

MS-DOSシステムコールは使用できない
    MS-WindowsはMS-DOSではないので、MS-DOSのシステムコールは当然のことながら 使用できない。

アセンブラ部分では、変数(データ領域)を作れない。
    すなわち、「DB」などは使用できない。もし変数を使いたい場合は、C++(C)言語の 部分で作成する。
  • 8bit(1byte)・・・char
  • 16bit(2byte)・・・short
  • 32bit(4byte)・・・long
書式) char　　変数名;
例　) char　　RED1;
定義した変数や定義済みの変数から値を読んだり、代入する場合は、今までの 方法と同じである。

書き方
    「_asm{」の前に必要な変数領域を作っておく。そして、「_asm{」と「}」の 間にアセンブラの部分を書く。


卒業試験

12-4.ネガポジ反転

ソースファイル
Nega.cpp

定義
void CGraphDoc::TranNegaAsm(LPSTR pDIB, DWORD width, DWORD height)

定義済み変数
  • pDIB・・・・・画像データのアドレス(32bits)
  • width・・・・・画像の横幅(32bits)
  • height・・・・画像の高さ(32bits)

機能
ネガ、ポジを反転する。

解説
    元の画像の輝度をx%とすると、求める輝度は「100-x」%となる。すなわち、今回の画像は RGBそれぞれ256階調であるので、「255-データ」を新しいデータとして、pDIBのアドレスに 格納すればよいことになる。

    なお、「255-データ」という式は、データを「NOT」で反転するのと同じことである。

注意
    これ以降全ての機能に当てはまることだが、「ゴミ」の部分には触らない。
    1バイトでも間違えると、Windowsに怒られるので注意。

ヒント
    ループで横幅分ネガポジ反転する。それが終わったらゴミの部分をとばす。これらの操作を 高さ分ループさせる。


卒業試験

12-5.明るくする、暗くする

ソースファイル
Bright.cpp

定義
void CGraphDoc::TranBrightAsm(LPSTR pDIB, DWORD width, DWORD height)
void CGraphDoc::TranDarkAsm(LPSTR pDIB, DWORD width, DWORD height)

定義済み変数
  • pDIB・・・・・画像データのアドレス(32bits)
  • width・・・・・画像の横幅(32bits)
  • height・・・・画像の高さ(32bits)

機能
明るさ変更

解説
    明るくする場合は、元の輝度をx%とすると「x*1.41」%にする。1.41倍というのは、 ２回この操作をすると、明るさが約２倍になる値である。このホームページでは、小数の演算は やっていないので、整数で何とかしなくてはならない。すなわち、元のデータを141倍し、 100で割るのである。100で割って、141倍すると、答えが変わってくるので注意。

    暗くするのも同様であり、輝度を0.71倍にする。これも２回行うと、元の半分の 明るさになる値である。

注意
    これ以降全ての機能に当てはまることだが、「ゴミ」の部分には触らない。
    1バイトでも間違えると、Windowsに怒られるので注意。

ヒント
    ループで横幅分明るさを変更する。それが終わったらゴミの部分をとばす。これらの操作を 高さ分ループさせる。


卒業試験

12-6.グレースケール

ソースファイル
Color.cpp

定義
void CGraphDoc::TranGrayAsm(LPSTR pDIB, DWORD width, DWORD height)

定義済み変数
  • pDIB・・・・・画像データのアドレス(32bits)
  • width・・・・・画像の横幅(32bits)
  • height・・・・画像の高さ(32bits)

機能
モノクロ画像に変換する

解説
    モノクロへの変換とは、RGB各色成分の値がすべて同じにすればよい。しかし、RGBそれぞれの 平均をとっただけでは、明るさが変わってしまう。なぜならば、緑は明るいが、青は暗いので、 それらの平均を取るのは、不公平だからである。もっと詳しく言うと、白の明るさを100%と すると、一番明るい赤の明るさは30%、同じく緑は59%、青は11%だからである。これらを 考慮して平均をとらなければならない。

    すなわち、以下の式になる。
Grayの輝度 ＝ 0.3 × 赤の輝度 ＋ 0.59 × 緑の輝度 ＋ 0.11 × 青の輝度

    こうして得られたGrayの輝度を、RGBの3バイトに書き込めば良い。


卒業試験

12-7.二値化

ソースファイル
Color.cpp

定義
void CGraphDoc::TranHiconAsm(LPSTR pDIB, DWORD width, DWORD height, DWORD level)

定義済み変数
  • pDIB・・・・・画像データのアドレス(32bits)
  • width・・・・・画像の横幅(32bits)
  • height・・・・画像の高さ(32bits)
  • level・・・・・しきい値(32bits)

機能
8色画像に減色する

解説
    RGBそれぞれを、0か255のどちらかにする。すなわち8色に減色することになる。
    それぞれの輝度がある値以上であれば255、それ未満であれば0にする。ある値のことを しきい値といい、変数levelで渡される。


卒業試験

12-8.上下反転、左右反転

ソースファイル
Rotate.cpp

定義
void CGraphDoc::TranVarAsm(LPSTR pDIB, DWORD width, DWORD height)
void CGraphDoc::TranHoriAsm(LPSTR pDIB, DWORD width, DWORD height)

定義済み変数
  • pDIB・・・・・画像データのアドレス(32bits)
  • width・・・・・画像の横幅(32bits)
  • height・・・・画像の高さ(32bits)

機能
上下反転、左右反転する

解説
    一番左(下)の点(3bytes)と一番右(上)の点を入れ換える。続いて、2番目、3番目・・・と 入れ換えていく。ただし、幅(高さ)分繰り返すと、せっかく反転したものが、さらに反転されて 元の画像に戻ってしまうので注意。要するに、幅(高さ)÷2回分繰り返し、それを高さ(幅)分 繰り返す。
    ちなみに、上下反転、左右反転を両方行うと、180°回転した画像になる。メニューバーには 180°回転というメニューもあるが、これは上下反転、左右反転を両方呼び出しているだけ なので、これら２つの機能を実装しないと、180°回転も行えない。


卒業試験

12-9.4分割

ソースファイル
Div.cpp

定義
void CGraphDoc::TranDivAsm(LPSTR pDIB, LPSTR pOut, DWORD width, DWORD height)

定義済み変数
  • pDIB・・・・・画像データのアドレス(32bits)
  • pOut・・・・・返還後の画像データの格納アドレス(32bits)
  • width・・・・・画像の横幅(32bits)
  • height・・・・画像の高さ(32bits)

機能
画像を1/4に縮小し、その画像を4つ張り付ける

解説
    縮小するアルゴリズムはバイキュービック法などいくつかある。縮小してもできるだけ 元の画像が歪まないようにするためのアルゴリズムである。1/nに縮小するアルゴリズムでは 簡単なものは、縦横それぞれnピクセル分の平均値を格納する方法、nの倍数分の座標の データのみを格納する方法がある。サンプルプログラムや解答では一番簡単な 後者の方法を採用している。

流れ
    今回は1/2にするので、x座標、y座標ともに偶数のピクセルをpDIBから読みとる。 そして、それをpOutに書き込む。4分割であるので、画像は4つになるわけだから、 書き込む先は4ヶ所になる。

    例えば、x,y=(10,20)のピクセルのデータを、pOutの(5,10)、(5+width÷2, 10)、 (5,10+height÷2)、(5+width÷2, 10+height÷2)の座標に書き込む。

ヒント
    これ以降の機能では、流れが複雑になってくるので、ある座標のデータを 読み込むサブルーチンを作ると便利である。

    例えば、X座標をEAXに、Y座標をEBXに格納して、そのサブルーチンを呼び出せば、 ALにR、BLにG、CLにBの輝度が格納され、EAXにpDIB(pOut)からのオフセットアドレス が格納され帰ってくる。また、指定したアドレスが、画像データの外であれば、 CF(キャリーフラグ)が1になる、といったサブルーチンである。解答例では、 このようなサブルーチンを作成してある。


卒業試験

12-10.モザイク

ソースファイル
mozaic.cpp

定義
void CGraphDoc::TranMosicAsm(LPSTR pDIB, DWORD width, DWORD height, DWORD pix)

定義済み変数
  • pDIB・・・・・画像データのアドレス(32bits)
  • width・・・・・画像の横幅(32bits)
  • height・・・・画像の高さ(32bits)
  • pix・・・・・・モザイクマトリックスのサイズ(32bits)

機能
モザイク加工を施す

解説
    モザイクとは、モザイクマトリックスと呼ばれる正方形の中を平均化することである。 すなわち、モザイクマトリックスが2ピクセルの場合は、2×2の4ピクセル分の平均を取り、 その4ピクセルに格納することで、モザイク化される。
? ? ? ? ? ?
1 | 2 | 3 | 4 | 5 | 6
7 | 8 | 9 | 10 | 11 | 12
? ? ? ? ? ?
→
? ? ? ? ? ?
4 | 4 | 6 | 6 | 8 | 8
4 | 4 | 6 | 6 | 8 | 8
? ? ? ? ? ?

流れ
    今回は流れがかなり複雑になると思う。今までの二重ループに加え、マトリックスの 計算をするのに、横、縦とさらに二重のループが必要になる。

ヒント
    例えば、上の例のようにモザイクマトリックスが2ピクセルで、画像の幅が奇数ピクセルの 場合、一番右が残ってしまう。このような場合は、マトリックスに足りないが、残った余り だけで平均化する。

    １つのデータは8bitsであるが、平均を取るための足し算は8bitsレジスタで行うと、 当然桁が足りなくなってしまう。マトリックスの大きさが16を越えると、16bitsレジスタ でも足りなくなってしまう。そこで計算はすべて32bitsレジスタを使う必要がある。


卒業試験

12-11.シャープ

ソースファイル
Sharp.cpp

定義
void CGraphDoc::TranSharpAsm(LPSTR pDIB, LPSTR pOut, DWORD width, DWORD height)

定義済み変数
  • pDIB・・・・・画像データのアドレス(32bits)
  • pOut・・・・・変換後の画像データの格納アドレス(32bits)
  • width・・・・・画像の横幅(32bits)
  • height・・・・画像の高さ(32bits)

機能
画像を鮮鋭化する

解説
    ぼやけた画像をくっきりさせる手法である。シャープ化にもいくつかのアルゴリズムが 知られているが、今回はその中でも最も簡単な方法を紹介する。
  • 計算したい座標の輝度を5倍する。
  • その値から、その点の上下左右の各点の輝度を引く
  • 計算結果をpOutの座標(1点のみ)に格納する
    ただし、計算したい点のまわりに点が無い場合は、自分自身を5倍ではなく、適当な 倍率にする。すなわち計算したい点が一番左下の場合は、自分自身を3倍し、右、上の 点の輝度を引く。

注意
    計算後の画像データは、pOutに格納することに注意
    計算は、16bitsレジスタ以上で行わないといけないことに注意


卒業試験

12-12.ぼかし

ソースファイル
Narrow.cpp

定義
void CGraphDoc::TranNarrowAsm(LPSTR pDIB, LPSTR pOut, DWORD width, DWORD height)

定義済み変数
  • pDIB・・・・・画像データのアドレス(32bits)
  • pOut・・・・・変換後の画像データの格納アドレス(32bits)
  • width・・・・・画像の横幅(32bits)
  • height・・・・画像の高さ(32bits)

機能
画像をぼかす

解説
    シャープの反対で、画像をぼかす方法である。これにもいくつかアルゴリズムがあるが、 ここでも簡単な方法を紹介する。

    計算したい点とその周りの9点の平均を取り、その結果をpOutの元の点(1点のみ)に 格納する。

    ただし、計算したい点のまわりに点が無い場合は、存在する点のみだけで計算する。

    今回は、3×3のマトリックスで計算するが、マトリックスを大きくし、5×5=25点の 平均にすれば、ぼやけ率が大きくなる。また、正方形ではなく、横、縦や斜めなど、 １直線で平均すれば、カメラを動かしながら撮影したような(ぶれたような)画像になるので 余力のある人は、挑戦してみて欲しい。

注意
    計算後の画像データは、pOutに格納することに注意
    計算は、16bitsレジスタ以上で行わないといけないことに注意


卒業試験

12-13.エンボス加工

ソースファイル
Embos.cpp

定義
void CGraphDoc::TranEmbosAsm(LPSTR pDIB, LPSTR pOut, DWORD width, DWORD height, DWORD xOff, DWORD yOff)

定義済み変数
  • pDIB・・・・・画像データのアドレス(32bits)
  • pOut・・・・・変換後の画像データの格納アドレス(32bits)
  • width・・・・・画像の横幅(32bits)
  • height・・・・画像の高さ(32bits)
  • xOff・・・・・画像のずれ(横方向)(32bits)
  • yOff・・・・・画像のずれ(縦方向)(32bits)

機能
エンボス加工を施す

解説
    エンボス加工とは、濃度差を利用して画像を浮き出たような効果を出す機能である。 ネガ像とポジ像を、xOff、yOffピクセル分だけずらして重ねることで実現する。

    詳しく解説すると以下のようになる。
  • 基準座標を(x,y)とすると、その点のネガを作成し、それをp1とする。
  • (x-xOff, y-yOff)の点の輝度をp2とする。
  • p1 + p2 - 128をpOutとする。
  • pOutがマイナスの場合は0にする。
  • pOutが255を越えた場合は255にする。
  • pOutを(x,y)に格納する。
注意
    計算後の画像データは、pOutに格納することに注意
    計算は、16bitsレジスタ以上で行わないといけないことに注意


卒業試験

12-14.ディザ拡散

ソースファイル
Color.cpp

定義
void CGraphDoc::TranDitherAsm(LPSTR pDIB, DWORD width, DWORD height)

定義済み変数
  • pDIB・・・・・画像データのアドレス(32bits)
  • width・・・・・画像の横幅(32bits)
  • height・・・・画像の高さ(32bits)

機能
ディザ拡散による8色への減色

解説
    ディザ拡散とは、できるだけ元の画像が歪まないように減色するアルゴリズムである。 後述するマトリックスの大きさや、しきい値によりいくつかの方法が知られているが、 ここではBayer型4×4マトリックスを使用した例を紹介する。
0 | 128 | 32 | 160
224 | 64 | 224 | 96
48 | 176 | 16 | 144
240 | 112 | 208 | 80
    上表がBayer型4×4マトリックスと呼ばれるしきい値である。

    計算したい座標が(x,y)だとする。xもyも4の倍数である場合のしきい値は、240である。 xを4で割ったあまりが2、yを4で割ったあまりが3の場合のしきい値は、32である。 このようにしきい値を求める。

    そして、(x,y)の輝度がしきい値以上であれば新しい輝度を255に、しきい値未満であれば 新しい輝度を0にする。これをRGB各色について計算する。


卒業試験

12-15.鉛筆を置いてください

    ジリリリリ。試験終了です。鉛筆を置いてください。

    みなさんできましたでしょうか？答えあわせ用に、 解答例を示します。参考にしてみてください。

    なお、質問等があればメールください。ただし、「こんな機能をつけたいけど、 アルゴリズム教えて」とかはダメです。「こんなアルゴリズムあるよ」という メールでしたら大歓迎です。質問はこのホームページおよび、この終了試験の プログラムのアセンブラの部分、私のフリーウェアー、その他・・・、に限ります。

    なお、質問には、できるだけ返事は書くつもりですが、私の知識を 越えるものや、時間の都合などにより、返事が出せなかったり、遅れたり するかもしれませんのでご了承ください。

    メールアドレスはjp1ecb@jarl.comです。


アセンブラ入門

トップ->アセンブラ入門

あなたは

人目のアセンブラの受講生です。

0. はじめに

1. 基礎
      1. レジスタとは
      2. セグメントとは

2. MOV命令
      1. MOV命令
      2. コンパイル
      3. 即値の転送
      4. メモリーへの転送
      5. 練習問題

3. 算術演算
      1. 符号なし演算と符号付き演算
      2. 足し算
      3. 練習問題
      4. フラグ
      5. 引き算
      6. かけ算
      7. わり算
      8. INC,DEC

4. 分岐
      1. 無条件分離
      2. 比較命令
      3. 条件分離
      4. 練習問題
      5. ループ
      6. サブルーチン
      7. スタックとは
      8. PUSH,POP
      9. 練習問題

5. 論理演算
      1. AND命令
      2. OR命令
      3. XOR命令
      4. NOT命令
      5. NEG命令
      6. TEST命令
      7. 練習問題

6. シフト演算
      1. シフト
      2. ローテート
      3. 練習問題1
      4. 練習問題2

7. ストリング命令
      1. ストリング命令とは
      2. MOVS?
      3. REP
      4. DFの設定
      5. LODS?
      6. STOS?
      7. SCAS?
      8. CMPS?
      9 .練習問題

8. 入出力
      1. 概要
      2. IN命令
      3. OUT命令
      4. INS,OUTS命令

9. その他
      1. LEA
      2. XCHG
      3. NOP

10. 割り込み
      1. 割り込みとは
      2. ソフトウェア割り込み
      3. ハードウェア割り込み
      4. 割り込みベクター
      5. 割り込み処理の作り方
      6. 練習問題

11. レジスタの型変換(キャスト)

12. 卒業試験
      1. 卒業試験
      2. 画像データの格納形式
      3. インラインアセンブラ
      4. ネガポジ反転
      5. 明るくする、暗くする
      6. グレースケール
      7. 二値化
      8. 上下反転、左右反転
      9. 4分割
      10. モザイク
      11. シャープ
      12. ぼかし
      13. エンボス
      14. ディザ拡散
      15. 鉛筆を置いてください

13. 終わりに

文字コード

アセンブラ　～MS-DOSの世界～

・ トップページに戻る


終わりに

    とうとう最後まできましたねぇ。お疲れさまでした。

    さて、ここまではただ単にアセンブラの命令を紹介してきただけです。これだけでは プログラムなんか作れません。ファイルのオープンさえ解説してないんですから。また、 コマンドラインからの引数(C言語などでいうmain関数の引数)も解説していません。 そこで、「アセンブラ　 ～MS-DOSの世界～」では、MS-DOSシステムコール、コマンドラインからの引数、 EMS,XMS,USBの使いかたを紹介します。


はじめに

    このページでは、「アセンブラ入門」を 読んでくれた方が、実際にプログラムを作る際に必要になる知識を紹介します。 アセンブラの命令は１命令が小さい機能しかないので、実際にプログラムを作るためには OSの力を借りないと、大きなプログラムは書けません。(書けますが、発狂します。) そこで、主にOSを紹介します。ここでは数年前まで一世を風靡していたMS-DOSについて 紹介します。ここで紹介する事柄はMS-DOS Ver 5.0以降の共通の仕様ですが、 大半(というか主要な機能はほとんど)MS-DOS Ver 3.0以降で共通ですし、 一部はそれ以前のMS-DOSでも動きます。MS-DOS Ver 4以下で動かないものは その都度示しますので、どの機能がサポートされて、どの機能がサポートされていないか わかると思います。
← 前ページへ　:　トップへ　:　次ページへ →


コマンドラインからの引数

1-1.コマンドラインからの引数

    コマンドラインからの引数とは、C言語では「main関数の引数」と呼ばれる。 ようするに、MS-DOSのプロンプトからプログラム名の後にスペースを空けて書く 文字列のことを言う。
A>TEST /?

A>・・・プロンプト
TEST・・実行プログラム名
/?・・・引数
    この引数は例えば、プログラムの動作モード(使い方の表示など)を指定したり、 開きたいファイル名などを指定するのに使われる。

    C言語では半角スペースを区切りとして、配列に格納されるが、アセンブラでは ある決まった領域に、入力したままの文字列として格納されている。

    ある領域とはPSP(プログラム・セグメント・プリフィックスの中である。) PSPとはプログラムが実行されるときの状態などが格納されており、COMファイルでは (このホームページで紹介するプログラムはすべてCOMファイル)CS:0000hから (CS=DS=ESである。)100hバイト存在している。EXEファイルではES:0000hからである。また、 後述するシステムコール(AH=62h)を使用して、 求めることもできる。

    そして、PSPの80h番地に文字数が1バイトで格納され、 81h番地から引数が格納され、'0Dh'で終わっている。 PSPの大きさは100hバイトなので、引数は最大でも7Ehバイトしか入らないことになる。

    以下に、引数を表示するプログラムを示す。表示は システムコール(AH=09)を使用しているので、そちらを参照してほしい。
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:
    mov	bx,80h
    add	bl,[bx]
    inc	bx
    mov	[bx],byte ptr 24h	;引数の最後の0Dhを24hに変える

    mov	ah,9
    mov	dx,81h
    int	21h              	;引数表示。ah=9はDS:DXから'24h'が現れるまでの部分を表示する

    mov	ax,4c00h
    int	21h              	;終了

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

実行例)
A>TEST ABCD.TXT
 ABCD.TXT
A>

PSPアドレスの取得

    AH = 62h
    INT 21h

    戻り値： BX ： 現在実行しているプログラムのPSPのセグメントアドレス

文字列の表示

    AH    = 9h
    DS:DX = 表示させたい文字列のアドレス
    INT 21h

    戻り値：なし


コマンドラインからの引数

1-2.自分の名前は？

    C言語では引数配列の0番目は自分のプログラム名が入っている。アセンブラでは 自分の名前を取得するのはちょっと面倒くさい。詳しくは 「環境変数」で説明するので、参照してほしい。

    簡単に説明すると、PSPの2Chに16bitで、環境変数領域のセグメントが格納されている。 環境変数とは、DOSプロンプトから「SET」と入力すると出てくる文字列である。この 領域は各設定がNULL(=00h)で区切られて文字列として格納されており(このようなNULLで 終わる文字列のことをASCIIZ文字列という)、自分自身のパス名は
  "最後の設定",00h,00h,01h,00h,"自分のパス名",00h
という形で格納されている。ここでは、実際のプログラム例を載せるので、参考にしてください。
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:
    mov	bx,2Ch
    mov	es,[bx] 	;DS <= 環境変数セグメント
    push	es
    pop	ds

    cld
    xor	di,di
    xor	ax,ax
    mov	cx,-1

SEARCH:
    repnz	scasb   	;NULLを探す
    cmp	[di],al 	;NULLが２つ続いたら次は自分のパス名
    jnz	SEARCH

    add	di,3
    mov	dx,di
    repnz	scasb   	;最後のNULLを24hに変更する
    mov	[di-1],byte ptr 24h

    mov	ah,9
    int	21h     	;表示

    mov	ax,4c00h
    int	21h     	;終了

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

実行例)
C:\BINS>TEST ABCD.TXT
C:\BINS\TEST.COM
C:\BINS>


メモリー管理

2-1.環境変数

    MS-DOSのコマンドプロンプトから「SET」と入力すると、下記のような文字が表示される。

  A:\WINDOWS>set
  PROMPT=$p$g
  winbootdir=C:\WINDOWS
  COMSPEC=C:\WINDOWS\COMMAND.COM
  TMP=D:\TEMP\
  TEMP=D:\TEMP\
  PATH=C:\WINDOWS;C:\WINDOWS\COMMAND;C:\JDK1.2\BIN
  windir=C:\WINDOWS
  CMDLINE=javac Test.java

  A:\WINDOWS>
    これを環境変数といい、プログラムやOSがこの領域を参照して使用する。 使い方によって、自分の使いやすい環境を整えることができる。環境変数自体の 説明は省略するので、MS-DOSのマニュアルなどを参照してほしい。

    上でも書いたように、この環境変数領域はプログラムが参照できる。 その参照の方法を説明する。

    まずプログラムが実行されると、PSPと呼ばれる100hバイトの領域がとられる。 COMファイルではCS(=DS=ES=SS)セグメントの0000h番地から始まる。EXEファイルは ESセグメントの0000h番地から始まる。また、システムコールのAH=62hを用いて PSPのセグメントを調べることもできる。

PSPアドレスの取得

    AH = 62h
    INT 21h

    戻り値： BX ： 現在実行しているプログラムのPSPのセグメントアドレス

例)
    mov	ah,62
    int	21h
    mov	es,bx	; <= ES:0000 : PSP領域

    このPSPの002Ch番地には環境変数領域のセグメントが格納されている。したがって、 ESにPSPのセグメントが格納されている場合に、環境変数領域のセグメントをDSに設定 するには下記のようにする。

例）
    mov	ds,[es:2ch]	; <= DS:0000 : 環境変数領域

    この環境領域には、上の例を見ると下記のように、１行が00hで区切られている 文字列となっている。このように00h(NULL)で区切られているような文字列をASCIIZと 呼ぶ。
例）
       | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F |
-------+-------------------------------------------------+-----+----+----+--
000000 | 50 52 4F 4D 50 54 3D 24-70 24 67 00 77 69 6E 62 |  PROMPT=$p$g winb
000010 | 6F 6F 74 64 69 72 3D 43-3A 5C 57 49 4E 44 4F 57 |  ootdir=C:\WINDOW
000020 | 53 00 43 4F 4D 53 50 45-43 3D 43 3A 5C 57 49 4E |  S COMSPEC=C:\WIN
000030 | 44 4F 57 53 5C 43 4F 4D-4D 41 4E 44 2E 43 4F 4D |  DOWS\COMMAND.COM
000040 | 00 54 4D 50 3D 44 3A 5C-54 45 4D 50 5C 00 54 45 |   TMP=D:\TEMP\ TE
000050 | 4D 50 3D 44 3A 5C 54 45-4D 50 5C 00 50 41 54 48 |  =C:\WINDOWS;C:\W
000070 | 49 4E 44 4F 57 53 5C 43-4F 4D 4D 41 4E 44 3B 43 |  INDOWS\COMMAND;C
000080 | 3A 5C 4A 44 4B 31 2E 32-5C 42 49 4E 00 77 69 6E |  :\JDK1.2\BIN win
000090 | 64 69 72 3D 43 3A 5C 57-49 4E 44 4F 57 53 00 43 |  dir=C:\WINDOWS C
0000A0 | 4D 44 4C 49 4E 45 3D 6A-61 76 61 63 20 54 65 73 |  MDLINE=javac Tes
0000B0 | 74 2E 6A 61 76 61 00 00-01 00 41 3A 5C 54 45 53 |  t.java  . A:\TES
0000C0 | 54 2E 43 4F 4D 00                               |  T.COM.

    そして、下線の「00 00 01 00」を区切りとして、それ以前が環境変数が格納され、 それ以降の１行に現在実行されているプログラム名がASCIIZで格納されている。


メモリー管理

2-2.練習問題

    MS-DOSのコマンドプロンプトから「SET」と入力すると、下記のような文字が表示される。

  A:\WINDOWS>set
  PROMPT=$p$g
  winbootdir=C:\WINDOWS
  COMSPEC=C:\WINDOWS\COMMAND.COM
  TMP=D:\TEMP\
  TEMP=D:\TEMP\
  PATH=C:\WINDOWS;C:\WINDOWS\COMMAND;C:\JDK1.2\BIN
  windir=C:\WINDOWS
  CMDLINE=javac Test.java

  A:\WINDOWS>
    このように、環境変数を表示するプログラムを作れ。


メモリー管理

2-2.練習問題

    MS-DOSのコマンドプロンプトから「SET」と入力すると、下記のような文字が表示される。

  A:\WINDOWS>set
  PROMPT=$p$g
  winbootdir=C:\WINDOWS
  COMSPEC=C:\WINDOWS\COMMAND.COM
  TMP=D:\TEMP\
  TEMP=D:\TEMP\
  PATH=C:\WINDOWS;C:\WINDOWS\COMMAND;C:\JDK1.2\BIN
  windir=C:\WINDOWS
  CMDLINE=javac Test.java

  A:\WINDOWS>
    このように、環境変数を表示するプログラムを作れ。

CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:
    mov	ah,62h
    int	21h
    mov	es,bx           ;DS ← PSP
    mov	ds,[es:2Ch]     ;DS ← 環境変数領域

    mov	si,-1
    mov	ah,6            ;AH ← 6(システムコール用)
DISP_10:
    inc	si
    mov	dl,[si]         ;表示する文字
    cmp	dl,0
    jz	DISP_20         ;NULLなら改行する
    cmp	dl,1
    jz	EXIT            ;1 なら終了
    int	21h
    jmp	short DISP_10   ;それ以外なら繰り返し
DISP_20:                          ;↓改行
    mov	dl,0Dh
    int	21h
    mov	dl,0Ah
    int	21h
    jmp	short DISP_10   ;↑改行
EXIT:
    mov	ax,4C00h
    int	21h             ;終了

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです


メモリー管理

2-3. メモリーの割り当て

    MS-DOSでは通常1MBytesのメモリーを管理できる。しかしその中にはVRAMやROMなどの 領域も含まれており、MS-DOS自身とユーザーが使えるメモリーは640KBytes弱になって しまう。より多くのメモリーを使いたいという要望から、UMBや EMS、XMSなどのメモリー管理方法が 提唱され、1MBytesの壁を破ってきた。これらについては後で説明する。

    ここでは、通常の640KBytesのメモリーを使う方法を説明する。メモリーはすべて OSが管理しており、プログラマーは使いたいときにOSに借りる。これはMS-DOSに限らず、 MS-WindowsやMac OS、N88-BASICも同じである。(管理の仕方は異なるが。)(組み込み系 OSのリアルタイムOSはそのOSによるらしい)

    MS-DOSが管理しているメモリーを借りるためにはMS-DOSにお願いしなければならない。 それにはAH=48hのシステムコールを使う。これにはBXに 借りたいメモリーの大きさを16バイト単位で入れる必要がある。(1000hバイトなら、 BXには100hを入れる。)また、割り当てたメモリーはどんな値が入っているかわからない ので、必要によって初期化する必要もある。

    ただし、現在実行中のプログラムがCOMファイルの場合、そのプログラムが空きメモリーを すべて使用している。そこでプログラムが使用しているメモリーの大きさを変更してやらなければ ならない。通常COMファイルは64KBytesあれば十分なプログラムが多い。それ以上使いたい場合は 上で説明したように、メモリーを割り当てて使う。したがって64KBytesに減らしてやればよいことに なる。しかし、普通のプログラムは64KBytesも使うことはないので、それ以下(プログラムの大きさ ＋PSPの大きさ(100hBytes＋スタックの大きさ)にする。COMファイルの場合、SPの値がデフォルトで 0になっているため、SPの値も変更しないと他人が使っているメモリーを知らないうちに破壊して しまう場合がある。そこで、SPを適当に変更してからメモリーの大きさを変更する必要がある。

メモリーの割り当て

    AH = 48h
    BX = 使いたいメモリー(バイト) ÷ 16
    INT 21h

    戻り値：CF = 0 の時 → AX : 割り当てられたメモリーのセグメントアドレス
            CF = 1 の時 → AX = 08 ：メモリーが足りない
                           BX      ：割り当て可能な最大メモリー(バイト) ÷ 16

メモリーの変更

    AH = 4Ah
    BX = 新しいメモリーの大きさ(バイト) ÷ 16
    ES = 変更するメモリーのセグメントアドレス
    INT 21h

    戻り値：CF = 0 の時 → 正常に実行された
            CF = 1 の時 → AX = 07 : メモリーの所有者でない
                           AX = 08 : 空き容量がない
                           AX = 09 : OSの管理外

    以下に1000hバイトのメモリーを確保するCOMファイルの例を示す。
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:

    mov	sp,offset _END  	;スタック領域を変更
    mov	bx,offset _END  	;↓自分自身のメモリーを
    mov	cl,4            	;　いくつにすればよいか
    shr	bx,cl           	;　計算する
    inc	bx              	;↑
    mov	ah,4ah
    int	21h             	;メモリー変更

    mov	ah,48h
    mov	bx,1000h / 16
    int	21h             	;メモリー割り当て
    jc	ERR01           	;エラーか？
    mov	es,ax
    call	DISP            	;割り当てたセグメント表示
    mov	ah,9
    mov	dx,offset MSG01 	;メッセージ表示
    int	21h
    mov	ax,4C00h
    int	21h             	;終了

ERR01:                            	;エラールーチン
    mov	ah,9
    mov	dx,offset ERRMSG01
    int	21h
    mov	ax,4CFFh
    int	21h

DISP:                           	;セグメント表示
    mov	cx,10h
    xor	dx,dx
    div	cx
    push	dx
    or	ax,ax
    jz	DISPE
    call	DISP
DISPE:
    pop	dx
    add	dl,'0'
    cmp	dl,'9'
    jbe	DISPE10
    add	dl,7
DISPE10:
    mov	ah,2
    int	21h
    ret

MSG01   	db	'が確保できました',0Dh,0Ah,24h
ERRMSG01	db	'空きメモリーがありません',0Dh,0Ah,24h
_STACK  	dw	20 dup(?)	;スタック領域
_END:

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです
実行例
  C:\>TEST.COM
  11C5が確保できました

  C:\>


メモリー管理

2-4. メモリーの解放

    必要なくなったメモリーは基本的に自分で解放しなくてはいけません。ただし、 プログラムが終了すると自動的に解放されます。(古いMS-DOSでは解放されないそうです。) しかし、お行儀のよいソフトというのは、「発つ鳥跡を濁さず」で、自分で確保した メモリーは、自分で解放するものです。(Javaなどの新しい言語はガーベージコレクター といって、プログラムが終了しなくても、不必要になったメモリーは勝手に解放されることが 保証されています)

メモリーの解放

    AH = 49h
    ES = 解放するメモリーのセグメントアドレス
    INT 21h

    戻り値：CF = 0 の時 → 正常に実行された
            CF = 1 の時 → AX = 07 : メモリーの所有者でない
                           AX = 09 : OSの管理外


メモリー管理

2-5. UMB

    UMBとはMS-DOS Ver.5.0で正式にサポートされたメモリーで(Ver.3.30頃から メモリーのデバイスドライバを作っているサードパーティーが独自にサポートしていた)、 640KBytesを越える通常ROMなどが格納されているアドレスにRAMを割り当てようと言う規格です。 例えば、PC-9801シリーズではN88-BASICのROMが搭載されています。しかし、MS-DOSを使うのには、 このROMは必要ありません。このように不必要なROMの場所にRAMを割り当てて、普通のメモリーの ように使おうというものです。

    ここでは、MS-DOS Ver.5.0以降のMS-DOSを前提としており、デバイスドライバーを組み込んで、 UMBが使える状態にあることを前提としています。

    UMBは場所が640KBytes以上のあること以外は、普通のメモリーとなんら変わりません。 したがって基本的には今まで紹介してきたシステムコールで割り当てたり、解放したり できます。ただし、ちょっと手続きが必要になります。

    MS-DOSはメモリーの確保を行う際に、どのようにメモリーを確保するか決めます。 決めますと言うよりは、プログラマーが指定できます。つまり、1)メモリーアドレスの下位から 割り当てる、2)無駄のないように必要最小のブロックから割り当てる、3)一番上位から 割り当てる、の３種類があります。デフォルトは1)です。これを指定するには、 AH=58hのシステムコールを使います。

アロケーションストラテジの取得

    AX = 5800h
    INT 21h

    戻り値：AX = アロケーションストラテジ

AXの意味
0000h : 最下位から割り当てる
0001h : 必要最小ブロックから割り当てる
0002h : 上位から割り当てる
0040h : UMBの最下位から割り当てる
0041h : UMBの必要最小ブロックから割り当てる
0042h : UMBの上位から割り当てる
0080h : 最下位から割り当てる(UMB優先)
0081h : 必要最小ブロックから割り当てる(UMB優先)
0082h : の上位から割り当てる(UMB優先)

000?hはUMBがあってもUMBは無いものとして処理します。
004?hはUMBがあればそこから割り当てます。UMBがなければエラーになります
       (MS-DOS Ver.5.0以降で利用可能)
008?hはUMBを優先して割り当てます
       (MS-DOS Ver.5.0以降で利用可能)

アロケーションストラテジの変更

    AX = 5801h
    BX = アロケーションストラテジ
    INT 21h

    戻り値：CF = 0 の時 → 正常に実行
            CF = 1 の時 → AX = 1 : BXの値が無効

BXの意味
0000h : 最下位から割り当てる(デフォルト)
0001h : 必要最小ブロックから割り当てる
0002h : 上位から割り当てる
0040h : UMBの最下位から割り当てる
0041h : UMBの必要最小ブロックから割り当てる
0042h : UMBの上位から割り当てる
0080h : 最下位から割り当てる(UMB優先)
0081h : 必要最小ブロックから割り当てる(UMB優先)
0082h : の上位から割り当てる(UMB優先)
    自分でアロケーションストラテジを変更する場合は、やはり自分の責任で元に戻さなくては いけません。これはプログラムが終了してもMS-DOSは戻してくれません。この値がおかしいと、 動作がおかしくなるプログラムもこの世の中にはたくさん存在するようです。


メモリー管理

トップ-> アセンブラ　～MS-DOSの世界～-> 第2章.メモリー管理-> 2-5. UMB

    実は、アロケーションストラテジを変更する前に、もう一つしなければならない設定が あります。それは普通のメモリー(コンベンショナルメモリー)とUMBをリンクするという 作業です。

UMBのリンク状態取得

    AX = 5802h
    INT 21h

    戻り値：AL = 00 : UMBがリンクされていない
            AL = 01 : UMBがリンクされている

UMBのリンク状態設定

    AX = 5803h
    BX = 00 : UMBをアンリンクする
    BX = 01 : UMBをリンクする
    INT 21h

    戻り値：CF = 0 の時 → 正常に実行
            CF = 1 の時 → AX = 1 : BXの値が無効

    何度も言うようですが、プログラムが終了するときには、元の状態に戻しましょう。 UMBがリンクされたままだと、動作がおかしくなるプログラムも存在します。
← 前ページへ　:　 トップへ　:　 次ページへ →


メモリー管理

    では、UMBを確保するプログラム例を示します。100hバイトのメモリーをUMBを優先して 確保しますが、なければコンベンショナルメモリーに確保し、そのセグメントを表示します。
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:

    mov	sp,offset _END  	;スタック領域を変更
    mov	bx,offset _END  	;↓自分自身のメモリーを
    mov	cl,4            	;　いくつにすればよいか
    shr	bx,cl           	;　計算する
    inc	bx              	;↑
    mov	ah,4ah
    int	21h             	;メモリー変更

    mov	ax,5800h
    int	21h              	;アロケーションストラテジの取得
    mov	STRATEGY,ax
;
    mov	ax,5802h
    int	21h             	;UMBのリンク状態取得
    mov	ah,0
    mov	UMBLINK,ax

    mov	ax,5803h
    mov	bx,1
    int	21h             	;UMBをリンク

    mov	ax,5801h
    mov	bx,82h
    int	21h             	;アロケーションストラテジ設定

    mov	ah,48h
    mov	bx,100h / 16
    int	21h             	;メモリー割り当て
    pushf                   	;とりあえずフラグを保存
;
    mov	ax,5801h
    mov	bx,STRATEGY
    int	21h             	;アロケーションストラテジを戻す

    mov	ax,5803h
    mov	bx,UMBLINK
    int	21h             	;UMBリンク状態を戻す

    popf
    jc	ERR01           	;メモリーを確保できたか？
    mov	es,ax
    call	DISP            	;割り当てたセグメント表示
    mov	ah,9
    mov	dx,offset MSG01 	;メッセージ表示
    int	21h

    mov	ah,49h
    int	21h             	;メモリー解放

    mov	ax,4C00h
    int	21h             	;終了

ERR01:                            	;エラールーチン
    mov	ah,9
    mov	dx,offset ERRMSG01
    int	21h
    mov	ax,4CFFh
    int	21h

DISP:                           	;セグメント表示
    mov	cx,10h
    xor	dx,dx
    div	cx
    push	dx
    or	ax,ax
    jz	DISPE
    call	DISP
DISPE:
    pop	dx
    add	dl,'0'
    cmp	dl,'9'
    jbe	DISPE10
    add	dl,7
DISPE10:
    mov	ah,2
    int	21h
    ret

UMBLINK 	dw	0
STRATEGY	dw	0
MSG01   	db	'が確保できました',0Dh,0Ah,24h
ERRMSG01	db	'空きメモリーがありません',0Dh,0Ah,24h
_STACK  	dw	20 dup(?)	;スタック領域
_END:

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです
実行例
  C:\>TEST.COM
  D1C5が確保できました

  C:\>


メモリー管理

2-6. メモリー管理

    ここではMS-DOSがメモリーを管理しているのかを解説します。

    まずコンベンショナルメモリーの下位には 割り込みベクターがあります。その次に、MS-DOS本体とデバイスドライバーが常駐し、 次にMS-DOSのCOMMAND.COMが常駐します。さらに、常駐プログラム(TSR)があり、空きメモリーと 続きます。

    MS-DOSはメモリーを「所有者」「大きさ」そしてMS-DOS Ver.5.0からは「名前」という 合計16バイトで管理しています。これをMCB(Memory Control Block)Cといいます。 MCBはOS内に「目次」のように一覧になっているのではなく、COMMAND.COMの常駐している 領域から、MCBに記録されている「大きさ」から次のMCBを探し、次のMCBを探し・・・と 最後まで続けているのです。

   １バイト | ２バイト | ２バイト | ３バイト | ８バイト
MCBの内容 | 識別子 | 所有者 | 大きさ | ゴミ | 名前
識別子が'M'の時は続きがある、'Z'の時はそのMCBで終了という意味
所有者が0の時は、空きメモリー。所有者がある場合は、そのプログラムのPSPのある セグメントアドレスが格納されている。

    一番最初のMCBを見つけるのは面倒なので、通常はCOMMAND.COMの常駐領域を調べて そこからMCBを追っていくという方法が採られています。COMMAND.COMの常駐領域には 「INT　2Fh」という割り込み先があります。したがって「INT　2Eh」の割り込みベクターを 調べて、そこからMCBを追います。割り込みベクターは以前解説したように、 0000:0000番地からあるのでそれを調べればわかるのですが、割り込みベクターを 不必要に書き換えたりすると、コンピュータが誤動作したりするので、MS-DOSが 調べたり、書き換えたりしてくれます。それには「AH=25h(設定)」 や「AH=35h(取得)」を使います

割り込みベクターの設定

    AH = 25h
    AL = 設定したい割り込み番号
    DS = 割り込み先のセグメントアドレス
    DX = 割り込み先のオフセットアドレス
    INT 21h

    戻り値：なし

割り込みベクターの取得

    AH = 35h
    AL = 設定したい割り込み番号
    INT 21h

    戻り値：ES = 割り込み先のセグメントアドレス
            BX = 割り込み先のオフセットアドレス

    これを用いて「INT　2Eh」の割り込み先＝COMMAND.COMの常駐先を調べ、 そこから追います。例えば、COMMAND.COMの常駐先のセグメントが1001h番地ならば、 COMMAND.COM常駐領域のMCBのセグメントは1000h番地になります。

    1000h番地のMCBを調べ、大きさのところに「100h」と入っていたら、 次のMCBのセグメントアドレスは1101h番地になります。これを識別子が 'Z'になるまで続ければ、MCBを終えたことになります。UMBがリンクされていれば、 UMBまで追うこともできます。


メモリー管理

    前ページで説明したMCBの追うプログラムを示します。 MCBのセグメントアドレスと所有者、大きさ、名前を表示します。UMBは表示しません。
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:
    mov	ah,9
    mov	dx,offset MSG01
    int	21h             	;枠表示

    mov	ax,352Eh
    int	21h             	;割り込みベクター取得
    mov	ax,es
    dec	ax
    mov	es,ax

NEXT_MCB:
    call	DISP_MCB_INFO   	;MCB情報表示
    cmp	[es:0],byte ptr 'Z'	;識別子を見てチェックする

    jz	EXIT
    mov	ax,es
    add	ax,[es:3]
    inc	ax              	;次のMCBを計算
    mov	es,ax
    jmp	short NEXT_MCB
EXIT:
    mov	ax,4c00h
    int	21h

;----------------------------------------
DISP_MCB_INFO:
    mov	ax,es
    call	DISP_HEX        	;MCB表示
    mov	ah,2
    mov	dl,' '
    int	21h

    mov	ax,[es:1]
    call	DISP_HEX        	;PSP表示
    mov	ah,2
    mov	dl,' '
    int	21h

    mov	si,8
    mov	ah,2
DISP_NAME:                      	;名前表示
    mov	dl,[es:si]
    cmp	dl,20h
    jb	DISP_NAME_END
    int	21h
    inc	si
    cmp	si,10h
    jz	DISP_NAME_END
    jmp	short DISP_NAME
DISP_NAME_END:
    mov	dl,' '
    int	21h
    inc	si
    cmp	si,11h
    jnz	DISP_NAME_END

    mov	ax,[es:3]
    call	DISP_DEC         	;サイズ表示

    mov	ah,2
    mov	dl,0Dh
    int	21h
    mov	dl,0Ah
    int	21h
    ret
;----------------------------------------
DISP_HEX:                           	;セグメント表示
    mov	cx,10h
    xor	dx,dx
    div	cx
    push	dx
    or	ax,ax
    jz	DISP_HEX_E
    call	DISP_HEX
DISP_HEX_E:
    pop	dx
    add	dl,'0'
    cmp	dl,'9'
    jbe	DISP_HEX_10
    add	dl,7
DISP_HEX_10:
    mov	ah,2
    int	21h
    ret
;---------------------------------------------
DISP_DEC:                           	;セグメント表示
    mov	cx,10
    xor	dx,dx
    div	cx
    push	dx
    or	ax,ax
    jz	DISP_DEC_E
    call	DISP_DEC
DISP_DEC_E:
    pop	dx
    add	dl,'0'
    mov	ah,2
    int	21h
    ret

MSG01    	db	' MCB  PSP  Name     Size',0Dh,0Ah
             db	'---- ---- -------- --------',0Dh,0Ah,24h
_STACK  	dw	30 dup(?)	;スタック領域
_END:

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

  C:\>test
   MCB  PSP  Name     Size
  ---- ---- -------- --------
  F0F F10 COMMAND  557
  113D F10 !・u     89
  1197 11AB >ﾂ8・    18
  11AA 11AB COMMAND  361
  1314 11AB          89
  136E 11AB          7
  1376 1519 .        15
  1386 1387 DOSKEY   401
  1518 1519 COMMAND  361
  1682 1519          89
  16DC 16ED          15
  16EC 16ED TEST     35090

  C:\>
お詫び：セグメントが3桁になった場合など、表示が少しへんですが、面倒なのでそのままです
このプログラムは後でも使うので、とっておいてください。


メモリー管理

    プログラムが終了してもデータ領域(自分で 「AH=48hのシステムコール」を使用し確保した領域)を残しておきたい 場合があります。しかし自分で確保した領域は、プログラムが終了するときに、 解放されてしまいます(MS-DOSのバージョンにもよりますが)。そこでプログラムが終了 しても解放されないように細工をしなければなりません。

    その細工とは、そのメモリーの「所有者」を変更することです。(所有者をFFFFhに する人が多いようです。)また、次に実行したプログラムが、そのデータを 見つけやすいように、「名前」も自分の名前に変更すべきです。

    こうすることにより、MS-DOSや他のプログラムによって勝手に削除されてしまう ことを防ぐことができます。


メモリー管理

2-7. 常駐プログラム(TSR)

    常駐プログラムとは、メモリー上にいつまでも残っていて、何かあると動作する プログラムです。「何か」とは、例えば時間がきたらとか、ディスクにアクセスしたら とか、キーボードが押されたらとか、なんでもいいわけです。

    メモリーに残らなければならないので、今までの「AH=4Ch」の システムコールでは、メモリーが解放されるため不適当です。そこで、 「AH=31h」のシステムコールを使用します。

常駐終了

    AH = 31h
    AL = 親プロセスに返すリターンコード
    DX = メモリーに残すサイズ(バイト) ÷ 16
    INT 21h

    戻り値：なし

PSPの領域からDXで設定した分だけメモリーに残ります

    以下に常駐プログラムの例を示します。これは「AH=2Ah」の システムコールである、日付の取得を上書きして、日付を取得しようとしたら、 強制的に1999年1月1日であると返します。しかもその日は金曜日であるのに、月曜日である と返します。したがって、下の表示結果のようになります。また、メモリーに常駐している ため、先ほど作ったプログラムでMCBの数が増えているのがわかります。

    このサンプル例は、解放するルーチンを備えていませんので、リセットするしか 解除する方法はありません。
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:
    jmp	short MAIN
TSR:                            	;常駐する部分
    cmp	ah,2ah
    jnz	_RETURN         	;日付取得か？
    mov	cx,1999         	;強制的に1999年
    mov	dx,0101h         	;強制的に1月1日
    mov	al,1            	;強制的に月曜日
    iret                    	;割り込み終了

_RETURN:                        	;正規のINT 21hに飛ぶ
    db	0EAh            	;この４行は決まり文句
_OFF	dw	?               	;実はFAR JUMPしている
_SEG	dw	?
MAIN:
    mov	ax,3521h
    int	21h
    mov	_SEG,es
    mov	_OFF,bx         	;割り込みベクター取得

    mov	dx,offset TSR
    mov	ax,2521h
    int	21h              	;割り込みベクター設定

    mov	dx,offset MAIN
    mov	cl,4
    shr	dx,cl
    inc	dx              	;残る大きさ計算
    mov	ah,31h
    int	21h             	;常駐終了

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

C:\>DATE
現在の日付は 1999-11-13 (土)
日付を入力してください(年-月-日):

C:\>DISPMCB
 MCB  PSP  Name     Size
---- ---- -------- --------
F0F F10 COMMAND  557
113D F10 !・u     89
1197 11AB >ﾂ8・    18
11AA 11AB COMMAND  361
1314 11AB          89
136E 11AB          7
1376 1519 .        15
1386 1387 DOSKEY   401
1518 1519 COMMAND  361
1682 1519          89
16DC 16EE          16
16ED 16EE DISPMCB  35089

C:\>HOOK21

C:\>DATE
現在の日付は 1999-01-01 (月)　　　　　←「HOOK21」が悪戯している
日付を入力してください(年-月-日):　　 ←1999-01-01は本当は金曜日

C:\>DISPMCB
 MCB  PSP  Name     Size
---- ---- -------- --------
F0F F10 COMMAND  557
113D F10 !・u     89
1197 11AB >ﾂ8・    18
11AA 11AB COMMAND  361
1314 11AB          89
136E 11AB          7
1376 1519 .        15
1386 1387 DOSKEY   401
1518 1519 COMMAND  361
1682 1519          89
16DC 16ED          15
16EC 16ED HOOK21   18     ←「HOOK21」が常駐している
16FF 1711 ﾍ!ｺ      16
1710 1711 DISPMCB  35054


メモリー管理

    前のページのプログラムは２つの欠陥があります。１つは前ページも書いたように 解放できないこと。２つめは常駐プログラムに不必要な環境変数領域を開放していないことです。

    １つめの解放方法ですが、フックしている割り込みベクター(今回はINT　21h)の 割り込みベクターを調べ、その領域が自分のプログラムと名前が同じかどうかを調べます。 同じならば、割り込みベクターを戻して、その領域を開放します。

    もし名前が自分と違っていた場合、MCBを追っていけば自分自身を見つけることが できるかもしれませんが、これを解放してしまうと、後から常駐した新しいTSRが 自分を呼びだしているかもしれません。(前のページ部プログラムの「_RETURN」から ４行のように・・・。)　しかし呼び出されたときに、呼び出し先が解放されていれば、 暴走してしまいます。このような場合は、解放できません。 　　２つめの、環境変数領域の開放ですが、普通のプログラムだと終了と同時にMS-DOSが 解放してくれます。しかし常駐プログラムのような「AH=31h」の システムコールを使用して終了する場合は、自分で解放しなければなりません。 これは以前説明した「AH=49h」のシステムコールを使用して解放できます。

    それではこの２点を改良した、「HOOK21」完成版(?)を示します。
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:
    jmp	short MAIN
TSR:                            	;常駐する部分
    cmp	ah,2ah
    jnz	_RETURN         	;日付取得か？
    mov	cx,1999         	;強制的に1999年
    mov	dx,0101h         	;強制的に1月1日
    mov	al,1            	;強制的に月曜日
    iret                    	;割り込み終了

_RETURN:                        	;正規のINT 21hに飛ぶ
    db	0EAh            	;この４行は決まり文句
_OFF	dw	?               	;実はFAR JUMPしています
_SEG	dw	?
MAIN:
    call	STAY_CHEK
    jnc	REMOVE
STAY:
    mov	ax,[ds:2Ch]
    mov	es,ax
    mov	ah,49h
    int	21h             	;環境変数領域解放

    mov	ax,3521h
    int	21h
    mov	_SEG,es
    mov	_OFF,bx         	;割り込みベクター取得

    mov	dx,offset TSR
    mov	ax,2521h
    int	21h              	;割り込みベクター設定

    mov	ah,9
    mov	dx,offset MSG01
    int	21h             	;メッセージ表示

    mov	dx,offset MAIN
    mov	cl,4
    shr	dx,cl
    inc	dx              	;残る大きさ計算
    mov	ah,31h
    int	21h             	;常駐終了
REMOVE:
    mov	ax,es
    inc	ax
    mov	es,ax

    mov	bx,offset _OFF
    mov	ds,[es:bx+2]
    mov	dx,[es:bx]
    mov	ax,2521h
    int	21h             	;割り込みベクターを戻す

    mov	ah,49h
    int	21h             	;メモリー解放

    push	cs
    pop	ds
    mov	ah,9
    mov	dx,offset MSG02
    int	21h             	;メッセージ表示

    mov	ax,4c00h
    int	21h
;----------------------------------------------
STAY_CHEK:
    mov	ax,3521h
    int	21h             	;割り込みベクター取得

    mov	ax,es           	;解放できるかのチェック
    dec	ax
    mov	es,ax
    mov	di,8
    cld
    mov	si,offset PRCNAME
    mov	cl,7

    repz	cmpsb            	;名前が一緒かのチェック
    or	cx,cx
    jz	_STAY
    stc                     	;解放できない
    ret
_STAY:                           	;解放できる
    clc
    ret
;------------------------------------------
PRCNAME	db	'HOOK21',0
MSG01	db	'常駐しました',0Dh,0Ah,24h
MSG02	db	'解放しました',0Dh,0Ah,24h
    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

C:\>DISPMCB
 MCB  PSP  Name     Size
---- ---- -------- --------
F0F F10 COMMAND  557
113D F10 !・u     89
1197 11AB >ﾂ8・    18
11AA 11AB COMMAND  361
1314 11AB          89
136E 11AB          7
1376 1519 .        15
1386 1387 DOSKEY   401
1518 1519 COMMAND  361
1682 1519          89
16DC 16EE          16
16ED 16EE DISPMCB  35089

C:\>HOOK21
常駐しました

C:\>DISPMCB
 MCB  PSP  Name     Size
---- ---- -------- --------
F0F F10 COMMAND  557
113D F10 !・u     89
1197 11AB >ﾂ8・    18
11AA 11AB COMMAND  361
1314 11AB          89
136E 11AB          7
1376 1519 .        15
1386 1387 DOSKEY   401
1518 1519 COMMAND  361
1682 1519          89
16DC 1701          16
16ED 16EE HOOK21   18             ←常駐している
1700 1701 DISPMCB  35070

C:\>HOOK21
解放しました

C:\>DISPMCB
 MCB  PSP  Name     Size
---- ---- -------- --------
F0F F10 COMMAND  557
113D F10 !・u     89
1197 11AB >ﾂ8・    18
11AA 11AB COMMAND  361
1314 11AB          89
136E 11AB          7
1376 1519 .        15
1386 1387 DOSKEY   401
1518 1519 COMMAND  361
1682 1519          89
16DC 16EE          16             ←解放されている
16ED 16EE DISPMCB  35089

C:\>


メモリー管理

    さて、常駐プログラムの話が出てきたので、メモリー管理とは関係ありませんが、 常駐プログラムの話をもう少ししたいと思います。

    常駐プログラムを作るときに、一番迷うのがトリガーを何にするかという問題です。 トリガーとは、常駐プログラムが処理を始めるきっかけです。

    先ほどの例では、「AH=2Aのシステムコール」 をトリガーにしました。どのようなトリガーがあるかを以下に列挙します。

タイマー
    DOS/Vパソコンは約55ms毎に「INT　8」が呼ばれます。これをフックします。

キーボード割り込み
    キーボードが押されると、「INT　9」が呼ばれます。これはDOS/Vでも、PC-9801 シリーズでも同じですが、どのキーが押されたかどうかの判断が異なります。

暇なときに実行
    キー入力待ちなどのシステムコールが呼ばれたがユーザーがキーを押さなかったときなど、 MS-DOSが暇なときには「INT　28h」が呼ばれます。一般に、常駐プログラムでは システムコールを使用できませんが、これをトリガーにしたときは、AH=01～0Ch以外の システムコールが使用可能です。

    他にも考えられるトリガーはあります。プログラマーのアイデア次第と言うところでしょうか？


メモリー管理

    常駐プログラムでは一般にとても厳しい制限があります。それは システムコールをしようしてならないというものです。(マァ、 常駐プログラムの種類によりますが)

    特にプログラムの実行中にも、制御が移る場合のある常駐プログラムは、 システムコールを使用すると暴走することもあります。しかし使用しても良い場合も あります。MS-DOSの動作の流れがわかれば、システムコールを使用しても良い場合、 悪い場合というのがわかりますが、なれるまではそれを判断することはできません。 そこで、使用しても良いか、悪いかというのを判断するシステムコールがあります。 このシステムコールはほとんどの場合で使用できるので、このシステムコールのみ 使用するならば、まあ安全でしょう。

システムコールが使用可能かどうかの検査(MS-DOS非公開)

    AH = 34h
    INT 21h

    戻り値：ES:BX = Indosフラグのアドレス

    Indosフラグとは、1バイトのフラグでMS-DOSの機能が使用可能かどうかを 示すフラグです。このフラグが0の時はMS-DOSの機能が使用可能(システムコールを 使用しても良い場合)で、1の時は使用不可能(システムコールを使用しては ならない場合)です。

    また、他にも制限というか、いろいろ面倒な作業があります。それらを列挙します。

スタック領域の確保
    常駐プログラムは、割り込み処理と同じでいつ処理するかわかりません。そこで、 PUSH, POP, CALL, RETなどスタックを使用する場合、呼び出しもとのスタック領域を 使用すると、呼び出しもとのスタックが残り少なかった場合、元のプログラム領域や データ領域を破壊する場合があります。もしMS-DOSが処理中の場合、MS-DOSを壊して しまう場合もあります。そこで、新たな自分専用のスタック領域を用意する方が 無難です。

リエントラント
    常駐プログラムに制御が移り、新しいスタック領域を用意し、そのスタック領域を 使用していたとします。そのときに新たな割り込みが発生し、同じ常駐プログラムが 呼ばれたとします。このときに、自分のスタックを使用し、正常に実行され、元の プログラムに戻ります。元のプログラムとは、１度目に呼ばれた自分自身なので、 そこに戻ると、スタック領域は２度目に呼ばれた自分によって破壊されていますので、 正常に動作できなくなってしまいます。したがって自分が実行中は、自分が呼ばれないように (呼ばれても良いように)しなくてはなりません。これはスタック領域だけではなく、 変数を保管する領域も同じです。

CTRL + Cが押された場合
    標準ではCTRL + Cが押された場合、プログラムを終了しMS-DOSに戻ります。常駐 プログラムが処理中に、これが押された場合どうなるかわからないので、CTRL + Cを 無効にする必要があります。その方法は後述します。

同じハードを使用する場合
    例えば、タイマー割り込みを使用する常駐プログラムが２つあったとします。 しかし、タイマーは１つしかないので、後にタイマーを使用し始めた方が有効になり、 最初に使っていた常駐プログラムは、呼ばれなくなってしまいます。これを回避するのは、 常駐プログラムが常駐する場合に気をつけるか、ユーザーが気をつけるしかありません。 しかし、良いプログラムというのは、ユーザーが何も気にしなくても使えるように 配慮するものです。

    このように常駐プログラムは割り込みと どうように、面倒なプログラムですので、作る場合は気をつけてくださいね。


標準入出力

3-1. １文字入力

    キーボードからの入力や、ディスプレイへの出力はもっとも基本的なものなので、 MS-DOSのシステムコールでも何種類かずつ用意されている。ここでは１文字入力の システムコールを解説する。

１文字入力

    AH = 01,06,07,08のいずれか
    DL = FFh(AH=06のときのみ)
    INT 21h

    戻り値：AL = 入力文字
            AL = 0 : 入力なし(AH=06の時のみ)

    それぞれの違いは下記の通りである。
AH キー入力があるまでの
内部ループ 		入力文字のエコーバック CTRL + C
01 | する | する | 受け付ける
06 | しない | しない | 受け付けない
07 | する | しない | 受け付けない
08 | する | しない | 受け付ける

内部ループ
キーが入力されるまで待つこと
エコーバック
入力された文字を表示すること
CTRL + C
入力を中止し、場合によってはプログラムが終了される

場合によって、これらを使い分ける。


標準入出力

3-2. キーボードバッファー

    MS-DOSはキーボードから入力があった場合、一度キーボードバッファーに 溜めておいて、前ページのシステムコールが実行された場合に、そのバッファーから 取り出してプログラムに返す。このバッファーの操作をここで解説する。

キーボードバッファーの検査

    AH = 0Bh
    INT 21h

    戻り値：AL=00h バッファーは空になっている
          ：AL=FFH バッファーに文字が入っている

バッファーを空にして入力

    AH = 0Ch
    AL = 01,06,07,08,0Ah(対応する入力処理をする)
    DL = 出力する文字(AL=06のときはFFh以外)
    INT 21h

    戻り値：AL=入力された文字

    AL=01,06,07,08,0Ah以外の場合は、バッファーを空にするだけ


標準入出力

3-3. １文字出力

    次に１文字出力のシステムコールを解説する。

１文字出力

    AH = 02,06のどちらか
    DL = 出力する文字(AH=06のときはFFh以外)
    INT 21h

    戻り値：なし

AH=02の場合
CTRL + Cを押した場合、プログラムが終了されMS-DOSに戻ります。
AH=06の場合
CTRL + Cを受け付けません。

場合によって、これらを使い分ける。


標準入出力

3-4. 文字列出力

    これは今までにも何度か登場したシステムコールです。

文字列出力

    AH = 09
    DS = 出力する文字列のセグメントアドレス
    DX = 出力する文字列のオフセットアドレス
    INT 21h

    戻り値：なし

    DS:DXから始まる文字列を「$(=24h)」があるところまで出力します。


標準入出力

3-5. 文字列入力

文字列入力

    AH    = 0Ah
    DS:DX = 入力する文字列を格納するバッファーアドレス
    INT 21h

    戻り値：なし

    入力文字列を格納するバッファーは下図のようになります。


標準入出力

3-6. 練習問題

キーボードから文字列を入力し、アルファベットの小文字があれば、 大文字に変換して、出力するプログラムを作れ。


標準入出力

3-6. 練習問題

キーボードから文字列を入力し、アルファベットの小文字があれば、 大文字に変換して、出力するプログラムを作れ。
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:
    mov	ah,0Ah
    mov	dx,offset BUFF
    int	21h             	;文字列入力

    mov	si,dx
    inc	si
    mov	ch,0
    mov	cl,[si]         	;CX ← 入力文字数
    inc	si              	;SI ← 入力文字格納アドレス
    mov	di,si           	;DI ← 入力文字格納アドレス

    mov	ah,6
    mov	dl,0Ah
    int	21h             	;改行

    mov	dx,si            	;DX ← 入力文字格納アドレス

TOUPPER:                        	;↓大文字に変換
    lodsb
    cmp	al,'a'
    jb	TOUPPER_20
    cmp	al,'z'
    ja	TOUPPER_20
    sub	al,20h
TOUPPER_20:
    stosb
    loop	TOUPPER         	;↑大文字に変換

DISP:
    mov	al,0Dh
    stosb
    mov	al,0Ah
    stosb
    mov	al,24h
    stosb                   	;改行コード等挿入

    mov	ah,9
    int	21h             	;表示

    mov	ax,4C00h
    int	21h             	;終了

BUFF	db	41,0,41 dup(?),?,?,?	;入力バッファー
    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

C:\>test
This is a pen.         ←入力
THIS IS A PEN.

C:\>


標準入出力

3-7. RS-232C

    補助入出力装置(RS-232C)の入出力を行います。そのためにはRS-232Cドライバを 組み込む必要があります。「INT　21h」を使用したRS-232Cのシステムコールはこれしか 用意されていませんが、拡張システムコール「INT　0DCh」を利用すると、もう少し 高機能なことができるようになっていますが、このホームページでは省略します。

補助入力装置からの１文字入力

    AH = 03
    INT 21h

    戻り値：AL = 入力された文字

補助出力装置への１文字出力

    AH = 04
    DL = 出力文字
    INT 21h

    戻り値：なし


標準入出力

3-8. プリンタ

    プリンタへの１文字出力を行います。プリンタドライバが組み込まれていることが 前提となります。

プリンタへの１文字出力

    AH = 05
    DL = 出力文字
    INT 21h

    戻り値：なし


ファイル操作

4-1. ファイルオープン

    アプリケーションのほとんどはファイルを読んだり、書いたりします。MS-DOSでは、 Ver.2.0までFCB(File Control Block)というものを利用してファイル操作を行っていました。 しかし、それ以降のMS-DOSではファイルハンドルというものを利用しています。FCBを 利用したファイル操作は、それ以降のMS-DOSでも使用することはできますが、使い方が 面倒なことや機能的な面で、互換性を保つ必要がある場合にのみ使用するように推奨されています。

    ファイルを読んだり、書いたりするためには、ファイルをオープンしなくてはなりません。 そして使い終わったファイルはクローズしなくてはなりません。

    ファイルのオープンするためには、ファイル名（パス名）とそのファイルをどのように 扱うかを指定します。そしてファイルをオープンした後は、ファイルハンドルという 番号で管理します。

    余談になりますが、MS-DOSのメモリー管理はセグメントで行っていましたが、MS-Windowsでは メモリー管理も、ウインドウ管理も、当然ファイルも、ほとんどの資源がハンドルという番号で 管理されています。その原点が、このファイルハンドルというわけです。

ファイルオープン

    AH = 3Dh
    AL = ファイルアクセスモード
    DS:DX = ファイル名（パス名）の格納アドレス
    INT 21h

    戻り値：CF = 0 のとき
                   AX = ファイルハンドル
            CF = 1 のとき
                   AX = 0001 : シェアリングモードが無効
                   AX = 0002 : ファイル名が無効またはファイルが存在しない
                   AX = 0003 : パス名が無効または存在しない
                   AX = 0004 : オープンしているファイル数が多すぎる
                   AX = 0005 : ディレクトリやボリュームをオープンしようとした。または
                               リードオンリーファイルをライトモードでオープンしようとした
                   AX = 000C : アクセスモードが無効

    ALの上位１ビットは「インヘリッドビット」と呼ばれるもので、次の３ビットは 「シェアリングモード」を表します。これらについての説明は省略しますが、 普通はすべてのビットが「０」にすると覚えておいてください。

    そして、下位４ビットは「アクセスモード」で、ファイルを読むだけで書き込みはしない場合は 「０」、ファイルを書くだけで読まない場合は「１」、読み込みも書き込みも行う場合は「２」を 設定します。

    DS:DXにはファイル名もしくはパス名が格納されているアドレスを指定しますが、ファイル名、 パス名の最後には「00h」を格納し、ASCIIZ文字列として格納していなければなりません。

    以上を設定し、「INT　21h」を呼び出し、CF=0で帰ってくれば、正常にファイルをオープン できたことになり、AXにファイルハンドルが返ってきます。もしCF=1ならば、エラーがあった ことを示します。

    エラーコード「4」では、CONFIG.SYSに記入されている「FILES = ??」の値が小さく、 ファイルハンドルの数が足りなくなってしまい、ファイルをオープンできなかったことを 示します。


ファイル操作

4-2. ファイルクローズ

    前ページではファイルオープンの仕方について解説しましたが、ファイルをオープンしたら 必ずファイルをクローズしなければなりません。書き込みを行った場合は、ファイルクローズを 行って初めて正常に書き込まれます。従ってファイルの書き込みを行って、ファイルをクローズ しないで終了した場合は、正常に書き込まれている保証が無くなります。読み込みや書き込みを 行わなくても、ファイルを開いた場合は必ずクローズしなくてはなりません。

ファイルクローズ

    AH = 3Eh
    BX = ファイルハンドル
    INT 21h

    戻り値：CF = 0 のとき
                   正常にクローズした
            CF = 1 のとき
                   AX = 0006 : ファイルハンドルが無効


ファイル操作

4-3. ファイルリード

    ファイルのリードはバイト単位で行います。１度に65535バイトまでしかリードできないので、 それ以上読み込む場合は、必要に応じて何度かリードしなくてはなりません。

    また、下記のシステムコ－ルを実行するとファイルポインタが読めたバイト数分増加します。 ファイルポインタとはファイルの中のアドレスのようなもので、ファイルの何バイト目を 指しているかを示すものです。

    ファイルがオープンされた直後は、ファイルポインタは０すなわちファイルの先頭を 指しています。そして100バイトリードしたとすると、ファイルポインタは100になります。 さらに100バイトリードしようとした場合、このファイルポインタの位置から100バイト 読みます。

    このファイルポインタはMS-DOSが管理しており、プログラマは直接参照したり操作したり することはできません。操作の仕方に関しては後述します。

リードハンドル

    AH = 3Fh
    BX = ファイルハンドル
    CX = リードするバイト数
    DS:DX = データの格納アドレス
    INT 21h

    戻り値：CF = 0 のとき
                   AX = リードできたバイト数
            CF = 1 のとき
                   AX = 0005 : ライトモードでオープンされている
                   AX = 0006 : ファイルハンドルが無効

    CF=1のエラー項目以外は正常にリードできたことになります。例えば、100バイトのファイルを 1000バイト読もうとしたときも、100バイト読めたので正常に読めたことになります。また、 ファイルポインタがファイルの一番最後(EOF = End Of File)を示している場合にリードしようと した場合も0バイト読めたと言うことで、エラーにはならないので、注意が必要です。


ファイル操作

4-4. ファイルライト

    ファイルのライトもバイト単位で行います。１度に65535バイトまでしかライトできない こともリードと同じすし、ファイルポインタが正常に書き込んだバイト数だけ進むのも 同じです

ライトハンドル

    AH = 40h
    BX = ファイルハンドル
    CX = ライトするバイト数
    DS:DX = データの格納アドレス
    INT 21h

    戻り値：CF = 0 のとき
                   AX = リードできたバイト数
            CF = 1 のとき
                   AX = 0005 : リードモードでオープンされている
                   AX = 0006 : ファイルハンドルが無効

    CF=1のエラー項目以外は正常にライトできたことになります。例えば、AX≠CX の時は、ディスクがいっぱいで書き込まなかったと考えられますし、AX=0の時も ディスクがいっぱいもしくは、ディスクが途中で抜かれたり、電源が切られたなどが考えられます。 いずれの場合もCF=0で返ってきますが、プログラマーは正常にライトできていないので、 エラーと判断したい例です。したがって、AXの値をチェックする 必要があります。


ファイル操作

4-5. 練習問題1

問題
コマンドラインからの引数としてファイル名を取得し、そのファイルの内容を 表示するプログラムを作れ。(MS-DOSのTYPEと同じもの)


ファイル操作

s

4-5. 練習問題1

問題
コマンドラインからの引数としてファイル名を取得し、そのファイルの内容を 表示するプログラムを作れ。(MS-DOSのTYPEと同じもの)
解答
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:
    mov	bx,80h          	;↓ファイル名をASCIIZにする
    add	bl,[bx]
    inc	bx
    mov	[bx],byte ptr 0 	;↑ここまで

    mov	cx,80h          	;↓ファイル名の先頭を求める
    mov	di,81h
    mov	al,' '
    repz	scasb
    dec	di
    mov	dx,di           	;↑ここまで

    mov	ax,3D00h
    int	21h             	;リードモードでファイルオープン
    mov	bx,ax
    jnc	FILE_READ       	;エラーでなければジャンプ

    mov	ah,9            	;↓エラールーチン
    mov	dx,offset ERRMSG01
    int	21h
    mov	ax,4c01h
    int	21h             	;↑ここまで

FILE_READ:
    mov	ah,3Fh
    mov	cx,1
    mov	dx,offset DATA
    int	21h             	;1バイトリード
    cmp	ax,1
    jnz	EOF

    mov	ah,2
    mov	dl,DATA
    int	21h             	;表示
    jmp	short FILE_READ 	;繰り返す

EOF:
    mov	ah,3Eh
    int	21h             	;ファイルクローズ

    mov	ax,4c00h
    int	21h             	;終了

DATA    	db	?
ERRMSG01	db	'指定されたファイルをオープンできません',0Dh,0Ah,24h
    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです


ファイル操作

4-6. ファイルポインタの移動

    ファイルポインタとは、ファイルの何バイト目かを示す値です。ファイルがオープン された直後は0すなわちファイルの最初を示しています。例えば100バイト、リードやライトを 行うと、ファイルポインタは100を示します。

    場合によっては、ファイルの最後にデータを追加したり、ファイルの途中からリードしたい 場合などもあります。その場合はファイルポインタを操作しなければなりません。しかし ファイルポインタはMS-DOSが管理している値なので、プログラマが勝手に参照したり 変更したりできませんが、このシステムコールを用いると、参照したり、変更することができます。

ファイルポインタの移動

    AH = 42h
    AL = 移動モード(0,1,2のいずれか)
    BX = ファイルハンドル
    CX:DX = 移動するバイト数(符号付き整数(2の補数表現))
    INT 21h

    戻り値：CF = 0 のとき
                   DX:AX = 移動後の位置(先頭からのバイト数)
            CF = 1 のとき
                   AX = 0001 : 移動モード(AL)が無効
                   AX = 0006 : ファイルハンドルが無効

移動モード
    移動モードとは、移動の際の基準を示すもので、
  • 0 = ファイルの先頭から
  • 1 = ファイルの現在位置から
  • 2 = ファイルの最後から
の３種類があります。

    ファイルの先頭からマイナス方向への指定はできませんが、ファイルの最後からプラスの 指定はできます。


ファイル操作

4-7. 練習問題2

問題
コマンドラインからの引数としてファイル名を取得し、そのファイルのサイズを表示する プログラムを作れ。


ファイル操作

4-7. 練習問題2

問題
コマンドラインからの引数としてファイル名を取得し、そのファイルのサイズを表示する プログラムを作れ。
解答
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .386
    ORG	100h

START:
    mov	bx,80h          	;↓ファイル名をASCIIZにする
    add	bl,[bx]
    inc	bx
    mov	[bx],byte ptr 0 	;↑ここまで

    mov	cx,80h          	;↓ファイル名の先頭を求める
    mov	di,81h
    mov	al,' '
    repz	scasb
    dec	di
    mov	dx,di           	;↑ここまで

    mov	ax,3D00h
    int	21h             	;リードモードでファイルオープン
    mov	bx,ax
    jnc	FILE_SIZE       	;エラーでなければジャンプ

    mov	ah,9            	;↓エラールーチン
    mov	dx,offset ERRMSG01
    int	21h
    mov	ax,4c01h
    int	21h             	;↑ここまで

FILE_SIZE:
    mov	ax,4202h
    xor	cx,cx
    xor	dx,dx            	;ファイルポインタを
    int	21h             	;ファイルの最後部に移動する

    push	ax
    push	dx
    mov	ah,3Eh
    int	21h             	;ファイルクローズ

    mov	ah,9
    mov	dx,offset MSG01
    int	21h
    pop	dx
    pop	ax

    xchg	ax,dx           	;↓DX:AXをEAXに変換する
    shl	eax,16
    mov	ax,dx           	;↑ここまで
    call	DISP            	;表示(EAXを10進数で表示するサブルーチン)

    mov	ah,9
    mov	dx,offset MSG02
    int	21h

    mov	ax,4c00h
    int	21h             	;終了

DISP:
    mov	ecx,10
    xor	edx,edx
    div	ecx
    push	edx
    or	eax,eax
    jz	DISPE
    call	DISP
DISPE:
    pop	edx
    add	dl,'0'
    mov	ah,2
    int	21h
    ret

DATA    	db	?
ERRMSG01	db	'指定されたファイルをオープンできません',0Dh,0Ah,24h
MSG01   	db	'指定されたファイルは',24h
MSG02   	db	'バイトです',0Dh,0Ah,24h

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです
実行結果

  C:\>SIZE COMMAND.COM
  指定されたファイルは54988バイトです

  C:\>


ファイル操作

4-8. ファイル作成

    ファイルを読んだり、書いたりするのもファイルがあっての物種です。ファイルを 作らなければ、ファイルを読んだり、書いたりする機能も行うことができません。ここでは ファイルを作成する方法を解説します。

ファイル作成

    AH = 3Ch,5Bhのどちらか
    DS:DX = 作成するファイル名を格納しているアドレス
    CX = ファイルの属性(アトリビュート)
    INT 21h

    戻り値：CF = 0 のとき
              AX = ファイルハンドル
          ：CF = 1 のとき
              AX = 03h パス名が無効
              AX = 04h ファイルを作成したが、オープンしているファイル数が多すぎて
                       ファイルハンドルを割り当てられない
              AX = 05h (AH=3Chの場合)
                       CXで与えられた属性が無効。もしくは
                       パス名で指定されたファイルがリードオンリーでファイルの作成に失敗した
              AX = 05h (AH=5Bhの場合)
                       アクセスできない
              AX = 50h (AH=5Bhの場合)
                       指定されたファイルはすでに存在している

    ファイルが作成されると、そのファイルをオープンし、ファイルハンドルが返されます。 失敗すると、CFがセットされAXにエラーコードが返されます。

    AH=3Chの場合、指定されたファイルがすでに存在している場合は、ファイルを空にします。 すなわち今までのファイルは捨てられ、0バイトのファイルが作られます。ただし、もとの ファイルがリードオンリー属性で、書き換え不可能な場合にはエラーになります。

    AH=5Bhの場合、指定されたファイルが存在しているとエラーコード=50hが返されます。

    ファイル属性については、後述します。

例
    mov	ah,3Ch
    mov	dx,offset FILE_NAME
    xor	cx,cx
    int	21h

PATH_NAME	db	'C:\WINDOWS\TEST.TXT',0


ファイル操作

テンポラリファイルの作成

    テンポラリファイルとは、一時的（そのプログラムが終了するまで）に使用するファイルです。 プログラムが終了すればそのファイルは必要なくなるので、特にユーザーやプログラマーが ファイル名を意識する必要はありません。

    前ページで解説したファイル作成では、ユーザーやプログラマーがファイル名を 指定できました。しかしテンポラリファイルを作成したい場合は、そのシステムコールを使用すると 同じファイル名が存在していた場合に、ユーザーの大事なデータファイルを消してしまう 可能性がありました。それを解決するためには、同じファイル名が存在するかを調べ、 存在していたら、別のファイル名でファイルを作らなければなりません。

    しかしこれは面倒な作業なので、ここで説明するシステムコールを使用します。 これはファイル名を指定する必要が無く、どこに作るかさえ指定すれば、MS-DOSが 勝手にファイル名をつけて作成してくれます。

テンポラリファイルの作成

    AH = 5Ah
    DS:DX = ファイルを作成するディレクトリ名が格納されているアドレス
    CX = ファイルの属性(アトリビュート)
    INT 21h

    戻り値：CF = 0 のとき
              AX = ファイルハンドル
          ：CF = 1 のとき
              AX = 03h パス名が無効
              AX = 04h ファイルを作成したが、オープンしているファイル数が多すぎて
                       ファイルハンドルを割り当てられない
              AX = 05h アクセスできない

    このシステムコールを実行すると、DS:DXで指定したファイルを作成するディレクトリの 後に13文字でファイル名が格納されます。(下記の例では、「FILE_NAME」)に ファイル名が格納されます。
例
    mov	ah,5Ah
    mov	dx,offset PATH_NAME
    xor	cx,cx
    int	21h

PATH_NAME	db	'C:\WINDOWS',0
FILE_NAME	db	13 dup(?)


ファイル操作

4-9. ファイル属性

    ファイル属性には狭義の属性と広義と属性があります。狭義は下記の上４つ。広義では 下記の６つを指します。しかし、下の２つは厳密には「ファイル」ではないので、 ファイル作成の属性は狭義を指します。

  • リードオンリー属性
  • 隠しファイル属性（ヒドゥン属性）
  • システムファイル属性
  • アトリビュート属性
  • ディレクトリ属性
  • ボリュームラベル属性

リードオンリー属性
「ファイルの削除」「書き込み」が行えなくなります。

隠しファイル属性
DIRコマンドなどでは表示されなくなります。
Windowsでは、「このフォルダのカスタマイズ」で設定しないと表示されなくなります。

システムファイル属性
主にMS-DOSのシステムが使用するファイルにつけられます。

アーカイブ属性
ファイルが更新されるとセットされます

ディレクトリ属性
ディレクトリにつけられます

ボリュームラベル属性
ボリュームラベル（ディスクの名前のようなもの）につけられます

ファイル作成で使う属性は狭義の属性（上記の上から４つ）です。
リードオンリー属性
 下位0bit目
隠しファイル属性
 下位1bit目
システムファイル属性
 下位2bit目
ボリュームラベル属性
 下位3bit目
ディレクトリ属性
 下位4bit目
アーカイブ属性
 下位5bit目
    通常はリードオンリーかどうかくらいしか使わないので、リードオンリーならば「１」、 そうでなければ、「０」を指定しますが、ファイル作成では リードオンリーにすると、ファイルを作成しても何も書けなくなるので、「０」にします。

    ファイル属性の変更の場合は、「リードオンリー＝１」、 「通常＝０」の２種類くらいでしょう。ファイル属性の変更で、ディレクトリ属性をセットして、 ファイルをディレクトリに変更するような使い方はできません。あくまでも取得の時に返されます。 つまり、ファイル属性の取得で、11hが返ってきたとすると、 リードオンリーのディレクトリということになります。


ファイル操作

4-10. ファイル削除

    不要なファイルは削除しないと、ディスク容量が少なくなります。ということで、 ここではファイルの削除について説明します。

ファイル削除

    AH = 41h
    DS:DX = 削除するファイル名を格納しているアドレス
    INT 21h

    戻り値：CF = 0 のとき
              正常に削除された
          ：CF = 1 のとき
              AX = 02h ファイル名が無効（存在しない）
              AX = 03h 指定されたファイルがリードオンリーまたはディレクトリ

    削除するファイルは、「通常ファイル」でなくてはなりません。リードオンリー属性の ファイルやディレクトリなどは削除できません。リードオンリー属性のついているファイルは 一度ファイル属性の変更を行って、リードオンリー属性を解除 しなければなりません。また、ディレクトリの場合はディレクトリの 削除という別のシステムコールを使用します。


ファイル操作

4-11. ファイル名変更

    ここではファイル名の変更方法を説明します。ファイル名だけでなく、ディレクトリ名の 変更もこのシステムコールを使用します。

ファイル名・ディレクトリ名の変更

    AH = 56h
    DS:DX = 古いファイル名を格納しているアドレス
    ES:SI = 新しいファイル名を格納しているアドレス
    INT 21h

    戻り値：CF = 0 のとき
              正常に変更された
          ：CF = 1 のとき
              AX = 02h DS:DXで指定されたファイル名が存在しない
              AX = 05h DS:DXで指定されたファイル名が無効　または、
                       ES:SIで指定されたファイルがすでに存在する。
              AX = 11h 古いファイル名と新しいファイル名が同じドライブでない

    指定したファイル名がファイルの時は、他のディレクトリに移動しながらファイル名の変更も可能です。
    しかし、異なるドライブ間はダメです。
    また、対象がディレクトリの場合は、その上のディレクトリが同じであれば可能です

例）
C:\WINDOWS\WIN.COM → C:\WINNT\WIN.COM ・・・可能(C:\WINNTというディレクトリが存在するとき)
C:\WINDOWS\WIN.COM → C:\WINNT\WIN.COM ・・・不可能(C:\WINNTというディレクトリが存在しないとき)
C:\WINDOWS\WIN.COM → D:\WINDOWS\WIN.COM ・・・不可能
C:\ABC\DEF → C:\ABC\GHI ・・・可能(DEFはディレクトリ)
C:\ABC\DEF → C:\JKL\GHI ・・・不可能(DEFはディレクトリ)


ファイル操作

4-12. ファイル属性の設定・取得

    ファイル属性に関しては前に述べたように、リードオンリーや システムファイルなどを表す属性です。つまり大事なファイルは削除できないようにリード オンリー属性をセットしたり、リードオンリー属性のファイルを削除したり書き換えたりする ためにリードオンリー属性を解除したりするためには、このシステムコールを使用します。

ファイル属性の取得

    AX = 4300h
    DS:DX = 属性を取得するファイル名を格納しているアドレス
    INT 21h

    戻り値：CF = 0 のとき
              CX = ファイル属性（詳細はこちら）
          ：CF = 1 のとき
              AX = 01h ALの値が無効
              AX = 03h 指定されたファイルが存在しない

ファイル属性の設定

    AX = 4301h
    CX = 設定するファイル属性（詳細はこちら）
    DS:DX = 属性を取得するファイル名を格納しているアドレス
    INT 21h

    戻り値：CF = 0 のとき
              正常に設定できた
          ：CF = 1 のとき
              AX = 01h ALの値が無効
              AX = 03h 指定されたファイルが存在しない
              AX = 05h CXで設定した値が無効

    ファイル属性についての詳細は（詳細はこちら）。

    このシステムコールを用いて、ファイルをディレクトリやボリュームファイルに 変更したり、その逆などは行えません。


ファイル操作

4-13. ファイル日時の設定・取得

    ファイル日時とは、ファイルを作成したり、変更したりした日付と時間のことを言います。

普通、ファイルを変更したり作成したりした場合は、MS-DOSがコンピュータの時計を利用して 勝手に書き換えますが、このシステムコールを用いることで、プログラマーが変更することも 可能です。

    このシステムコールはファイルをオープンしている状態で用います。しかし、この システムコールを用いて日付を変更しても、また書き換えてしまったのではそちらの 日付が書き込まれてしまいますので、このシステムコールはファイルをクローズする直前に 行わなくてはなりません。

ファイル日時の取得

    AX = 5700h
    BX = ファイルハンドル
    INT 21h

    戻り値：CF = 0 のとき
              CX = ファイルの時刻
              DX = ファイルの日付
          ：CF = 1 のとき
              AX = 01h ALの値が無効
              AX = 06h BXで指定したファイルハンドルが無効

ファイル日付の設定

    AX = 4301h
    BX = ファイルハンドル
    CX = 設定するファイルの時刻
    DX = 設定するファイルの日付
    INT 21h

    戻り値：CF = 0 のとき
              正常に設定できた
          ：CF = 1 のとき
              AX = 01h ALの値が無効
              AX = 06h BXで指定したファイルハンドルが無効

    日付と時刻のフォーマットは下記の通りです。
    時刻は、上位5bitで、「時」を0～23で表します。
    次の6bitで「分」を0～59で表します。
    そして、下位5bitで、「秒」を0～29で表します。これを２倍したものが実際の 「秒」になります。
    すなわち、CX=0001100001100011b(2進数)とすると、3時3分6秒になります。

    日付は、上位7bitで「年」を0～99で表します。0にすると、1980年で、 99にすると、2079年です。
    次の4bitで「月」を0～12で表します。
    そして下位5bitで「日」を0～31で表します。
    すなわち、DX=0000011001100011b(2進数)とすると、1983年3月3日になります。


ファイル検索

4-14. ファイル検索

    ファイルの検索を行うシステムコールを紹介します。検索といっても、どのディレクトリに あるかどうかを調べるものではありません。カレントディレクトリもしくは指定したディレクトリに 指定したファイルがあるかどうかを調べるものです。ファイル名の指定にはワイルドカードが 使えます。

    ワイルドカードとは、「*」「?」のことで、「*」は任意の0文字以上の文字(列)、「?」は 任意の１文字に置き換えられます。すなわち「ABC*.TXT」は、「ABC.TXT」、「ABCD.TXT」などが 当てはまります。「ABC?.TXT」は「ABC.TXT」は当てはまりません。また、「*.*」 とすれば すべてのファイルが当てはまります。

最初に一致するファイル名の検索

    AH = 4Eh
    DS:DX = 探すファイル名の格納アドレス
    CX = ファイル属性(詳細はこちら)
    INT 21h

    戻り値：CF = 0 のとき
              正常に実行された
          ：CF = 1 のとき
              AX = 02h DS:DXで指定したパスが無効
              AX = 12h 該当するファイルは存在しない

    ファイル属性についてはすでに解説した。ここで指定する ファイル属性は特別なファイルを検索する場合にのみ使用する。すなわち「dir」コマンドで 通常表示されないファイルの検索を行う場合で、具体的には「システムファイル」、 「隠しファイル」そしてディレクトリを指します。「リードオンリー」、「アーカイブ」 属性のファイルは特に指定しなくても勝手に探し出してくれます。

    すなわち、通常のファイル、リードオンリーファイルのみを探したい場合は、 CX=0で構いません。システムファイル、隠しファイルも検索したい場合は、CX=6を ディレクトリのみ検索したい場合はCX=10h、すべて検索したい場合はCX=16hを指定します。

    検索し、指定のファイルを見つけた場合にはDTAという領域に、 ファイル名などの情報が書き込まれます。DTAは標準ではPSPの 80h番地から始まっています。またDTAはプログラマーが設定したり、取得したりすることも 可能です。(詳細はこちら)

    DTAの1Eh番地からASCIIZ文字でファイル名が格納されていますので、これを利用して ファイルをオープンしたり処理を行うことができます。また、DTAの16h番地にファイルの 時刻、18h番地にファイルの日付が格納されています。

    ワイルドカードを使用した場合、該当するファイルが複数あっても１つしか情報は 得られません。どの順番に得られるかは解説するのが難しいので、ここでは不定と覚えて おいてください。そして、複数該当するファイルがあり、次のファイルの情報が得たい場合は 次のシステムコールを使用します。

次に一致するファイル名の検索

    AH = 4Fh
    INT 21h

    戻り値：CF = 0 のとき
              正常に設定できた
          ：CF = 1 のとき
              AX = 12h 該当するファイルは存在しない


ファイル操作

4-15. 練習問題3

問題
カレントディレクトリに存在するファイルを表示せよ。


ファイル操作

4-15. 練習問題3

問題
カレントディレクトリに存在するファイルを表示せよ。
解答
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:
    mov	ah,4eh
    mov	dx,offset FILE
    mov	cx,10h
    int	21h             	;最初に一致するファイル名検索
MAIN:
    jc	BYEBYE          	;存在しなければ終了
    call	DISP            	;表示
    mov	ah,4fh
    int	21h             	;次に一致するファイル名検索
    jmp	short MAIN
BYEBYE:
    mov	ax,4c00h
    int	21h             	;終了

DISP:                            	;↓ファイル名表示
    mov	ah,6
    mov	si,offset 80h + 1Eh
    cld
DISP_MAIN:
    lodsb
    cmp	al,0
    jz	DISP_END
    mov	dl,al
    int	21h
    jmp	short DISP_MAIN
DISP_END:                       	;　↓改行
    mov	dl,0Dh
    int	21h
    mov	dl,0Ah
    int	21h             	;  ↑ここまで
    ret                       	;↑ここまで

FILE	db	'*.*',0
    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです


ファイル操作

4-16. 予約されているファイルハンドル

    ファイルハンドルはファイルを操作するための識別番号というかID番号みたいなものである ことはすでに説明した。実は予約番号というのが5つ存在し、それらはMS-DOSが起動したときに オープンされている。プログラム中で使用してもクローズする必要はない。それらをここでは 紹介する。
ハンドル番号 | デバイス名 | 機　能
0 | CON | 標準入力(キーボード)からの入力
1 | CON | 標準出力(ディスプレイ)への出力
2 | CON | 標準エラー出力(ディスプレイ)への出力
3 | AUX | 標準補助装置(RS-232C)の入出力
4 | PRN | プリンタへの出力
    MS-DOSではファイル操作と同じ方法で多くのデバイスも操作する。(これもUNIXをまねている仕様) 例えば、キーボードからの入力は、ファイルオープンと同じシステムコールを 用いて、「CON」デバイスをオープンし、ファイルリードを行ってもよい。 また、ディスプレイへの出力も同様に、「CON」デバイスをオープンし、 ファイルライトを行ってもよい。

    ただし、通常CONデバイスは上の表のようにMS-DOSが起動したときからオープンされているので、 オープンする必要はなく、直接リード、ライトを行うことにより、キーボードからの入力、 ディスプレイへの出力が行える。
    さらにいくつかの予約されたデバイス名(ファイル名と等価)がある。
デバイス名 | 機　能
NUL | 何もしない
AUX | RS-232C
CLOCK | 時計
    これらは、オープンすれば使用可能になるデバイスである。 例えば、NULをオープンすればNULデバイスが使用可能となり、AUXをオープンすれば、 RS-232Cが使用可能になる(ただしRS-232Cのデバイスドライバが必要)。ここで紹介した デバイスはMS-DOSの予約デバイス名で、したがって、NULというファイルは作れないことになる。

    また、デバイスドライバを組み込み新たなデバイスを追加すれば、どうように ファイルではなくデバイスをファイルと同様に使用することも可能である。(この ホームページではデバイスドライバの作成方法は省略する。)

    さて、NULデバイスとはどういう機能を有するデバイスかというと、何もしない デバイスである。ここからリードすればEOF(ファイル終了)が返ってくるし、この デバイスにライトすれば、エラーにはならないが、ディスプレイにもファイルにも 何も出力されない。また、AUXデバイスはRS-232Cからの入出力が行えるデバイスである。


ファイル操作

4-17. ファイルハンドルの二重化(リダイレクト)

  C:\>DIR > DIR.TXT

  C:\>
    リダイレクトというのは、上記のような「>」を用いて本来ディスプレイに表示される メッセージをファイルにする機能をいう。ファイルにするだけでなく、何もしたくなければ、 NULデバイス「DIR > NUL」とする。正確には、標準入出力をファイルもしくは デバイスにすり替える機能のことをリダイレクトという。また、「> NUL」 のようにディスプレイに何も表示させないことを特にヌルリダイレクトと呼ぶ。

    上記の例では標準出力をディスプレイから「DIR.TXT」というファイルにすれ違えて 「DIR」を実行させた例である。逆に標準入力をキーボードからファイルにすることも できる。

  C:\>COPY CON ABC.TXT
  ABCDE
  123
  ^Z
          1 個のファイルをコピーしました.

  C:\>TYPE ABC.TXT
  ABCDE
  123

  C:\>
    パイプという機能は、次のように「|」を使う機能である。
  C:\>COPY CON ABC.TXT
  HIROKO
  ERIKO
  TAKAKO
  HITOE
  ^Z
          1 個のファイルをコピーしました.

  C:\>TYPE ABC.TXT | SORT
  ERIKO
  HIROKO
  HITOE
  TAKAKO

  C:\>
    上記の例は、「ABC.TXT」を作り、「TYPE」コマンドを用いて その内容を表示させているが、パイプで「SORT」コマンドをつなげて、 ソートして表示させている。

    どのように実行されているかというと、リダイレクトを用いて標準出力がテンポラリファイルに すり替えられて「TYPE」が実行される。そして、標準入力がそのテンポラリ ファイルにすり替えられて「SORT」が実行される。これにより、上記のような 結果になる。


ファイル操作

    前ページで紹介した、リダイレクトやパイプは次のシステム コールを用いて実現できる。

ファイルハンドルの二重化

    AH = 45h
    BX = 二重化するファイルハンドル
    INT 21h

    戻り値：CF = 0 の時
                AX = 新しいファイルハンドル
            CF = 1 の時
                AX = 04 : オープンされているファイルハンドルが多すぎる
                AX = 06 : BXで指定したファイルハンドルはオープンされていない
    例えば、「ABC.TXT」というファイルがオープンされており、ファイル ハンドルが、10だとすると、「BX=10」でこのシステムコールを呼び出す。 すると「AX=11」が返ってきたとすると、ファイルハンドル=10でも11でライトを行っても 同一のファイルに書き込むことができる。

    また、一方のファイルハンドルのファイルポインタが移動するともう一方も同様に 移動する。

    当然ながら、MS-DOS予約のファイルハンドル(標準入出力)も二重化することができる。


ファイル操作

ファイルハンドルの二重化

    AH = 46h
    BX = 二重化するファイルハンドル
    CX = 新しいファイルハンドル
    INT 21h

    戻り値：CF = 0 の時
                正常に実行された
            CF = 1 の時
                AX = 04 : オープンされているファイルハンドルが多すぎる
                AX = 06 : BXで指定したファイルハンドルはオープンされていない

    前ページのシステムコールとは異なり、新しいファイルハンドルを 指定できる。BXにオープンされているファイルハンドルを指定し、CXに新しいファイルハンドルを 指定する。CXで指定したファイルハンドルがオープンされている場合はクローズして、BXの ファイルハンドルを二重化したものが入る。

    すなわち、「DIR」コマンドの結果を「DIR.TXT」に 保存する場合は次のようにする。(つまり「C:\>DIR > ABC.TXT」)
標準出力をファイルにすり替えるリダイレクト
  • 「ABC.TXT」をオープンし、そのファイルハンドルをBXに代入
  • CXに標準出力のファイルハンドルを代入
  • ファイルハンドルの二重化を行う
    (これで、標準出力に出力をすると、ディスプレイではなくて「ABC.TXT」に書かれる。)
  • 「COMMAND.COM /C DIR」を実行する
  • 「ABC.TXT」をクローズする
標準入力をファイルにすり替えるリダイレクト
  • 入力用ファイルをオープンし、そのファイルハンドルをBXに代入
  • CXに標準入力のファイルハンドルを代入
  • ファイルハンドルの二重化を行う
    (これで、標準入力から入力すると、キーボードではなくてファイルから読まれる。)
  • プログラムを実行する
  • 入力用ファイルをクローズする
パイプ
  • 標準出力のファイルハンドルを二重化し、保管する
  • テンポラリファイルを作成し、そのファイルハンドルをBXに代入
  • CXに標準出力のファイルハンドルを代入
  • ファイルハンドルの二重化を行う
    (これで、標準出力に出力をすると、ディスプレイではなくてテンポラリファイルに書かれる。)
  • プログラムを実行する
  • 保管した標準出力のファイルハンドルをBXに代入する
  • CXに標準出力のファイルハンドルを代入
  • ファイルハンドルの二重化を行う
    (これで、標準出力が元の状態に戻った)
  • 標準入力のファイルハンドルを二重化し、保管する
  • テンポラリファイルのファイルハンドルをBXに代入
  • 標準入力のファイルハンドルをCXに代入
  • ファイルハンドルの二重化を行う
    (これで、標準入力から入力すると、キーボードではなくてファイルから読まれる。)
  • プログラムを実行する
  • 保管した標準入力のファイルハンドルをBXに代入する
  • CXに標準入力のファイルハンドルを代入
  • ファイルハンドルの二重化を行う
    (これで、標準入力が元の状態に戻った)
  • テンポラリファイルをクローズし、削除する


ディレクトリ操作

5-1. ディレクトリ

    ディレクトリ操作の次の操作はファイル操作と同じシステム コールを用いますので、そちらを参照してください。
  • ディレクトリ名変更
  • ディレクトリ属性設定・取得
  • ディレクトリ日時設定・取得
  • ディレクトリ検索
    また、ディレクトリ特有のシステムコールは下記のようなものがあります。

  • カレントディレクトリ変更
  • カレントディレクトリ取得
  • ディレクトリ作成
  • ディレクトリ削除


ディレクトリ操作

5-2. カレントディレクトリ変更

    ディレクトリとは最近ではフォルダともいい、ファイルやプログラムを整理して 格納するためのもので、UNIXから採用されたシステムです。

    MS-DOSのプロンプトから「CD」コマンドを用いることで、現在のディレクトリから 他のディレクトリに移ることが可能ですが、本システムコールではこの「CD」コマンドに 相当するものです。

カレントディレクトリの変更

    AH = 3Bh
    DS:DX = 移動するディレクトリ名の格納先
    INT 21h

    戻り値：CF = 0 のとき
                   成功
            CF = 1 のとき
                   AX = 0003 : ディレクトリ名が無効

    ディレクトリ名は絶対パス('\'から始まるパス)、または相対パス (現在のディレクトリからのパス)のどちらでも構いません。


ディレクトリ操作

5-3. カレントディレクトリ取得

    プログラムによっては、カレントディレクトリを取得したい場合があります。 このシステムコールを使うと、指定ドライブもしくはカレントドライブのカレント ディレクトリ(絶対パス)をASCIIZ文字列として取得することができます。

カレントディレクトリの取得

    AH = 47h
    DL = ドライブ番号
    DS:SI = ディレクトリ名を格納するバッファー
    INT 21h

    戻り値：CF = 0 のとき
                   成功
            CF = 1 のとき
                   AX = 000F : ドライブ番号が無効

    このシステムコールでは、ドライブ名やルートディレクトリを示す最初の'\'は 含まれていません。したがって、「C:\WINDOWS\SYSTEM」がカレントディレクトリの場合、 DS:SIには「WINDOWS\SYSTEM」が返されます。

    ドライブ番号(DL)は次のように指定します。
DL
ドライブ名
0 | カレントドライブ
1 | A
2 | B
3 | C
： ：
    カレントディレクトリが長くて、64Byte以上ある場合は、エラー(AX=000Fh)が返されます。


ディレクトリ操作

5-4. ディレクトリ作成

    次にディレクトリの作り方について説明します。

ディレクトリの作成

    AH = 39h
    DS:DX = ディレクトリ名の格納アドレス
    INT 21h

    戻り値：CF = 0 のとき
                   成功
            CF = 1 のとき
                   AX = 0003 : ディレクトリ名が無効
                   AX = 0005 : ディスクがいっぱいか、ライトプロテクトがかかっている
                               もしくは同名のディレクトリもしくはファイルが存在する

    絶対パスでかかれている場合にはそのディレクトリが作られます。絶対パスに ドライブ名を含めることも可能です。相対パスの場合は、カレントドライブのカレント ディレクトリからの相対パスとして作成されます。

    また、一度に２つのディレクトリを作成することはできません。すなわち、 「C:\ABC」というディレクトリが存在しないのに、DS:DXに 「C:\ABC\123」を作成することはできません。


ディレクトリ操作

5-5. ディレクトリ削除

    次にディレクトリの削除の仕方について説明します。

ディレクトリ削除

    AH = 3Ah
    DS:DX = ディレクトリ名の格納アドレス
    INT 21h

    戻り値：CF = 0 のとき
                   成功
            CF = 1 のとき
                   AX = 0003 : ディレクトリ名が無効
                   AX = 0005 : ディレクトリが空ではないかライトプロテクトがかかっている
                               もしくはそのディレクトリが存在しない。または
                               そのディレクトリはルートディレクトリである
                   AX = 0010 : 指定したディレクトリはカレントディレクトリである

    削除するには下記のような条件があります。
  • カレントディレクトリは削除できない
  • ルートディレクトリは削除できない
  • ディレクトリの中身は空でなければならない


ディレクトリ操作

5-6. 練習問題

問題
    キーボードから文字列を読み込み、そのディレクトリを作成して、カレントディレクトリ をそのディレクトリに移動するプログラムを作れ。


ディレクトリ操作

5-6. 練習問題

問題
    キーボードから文字列を読み込み、そのディレクトリを作成して、カレントディレクトリ をそのディレクトリに移動するプログラムを作れ。
解答
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:
    mov	ah,9
    mov	dx,offset OP_MSG
    int	21h             	;プロンプト表示

    mov	ah,0Ah
    mov	dx,offset BUFF
    int	21h             	;ディレクトリ名入力

    mov	bh,0            	;↓ASCIIZに変換
    mov	bl,[BUFF+1]
    add	dx,2
    add	bx,dx
    mov	[bx],byte ptr 0 	;↑ここまで

    mov	ah,39h
    int	21h             	;ディレクトリ作成
    jc	FAIL

    mov	ah,3Bh
    int	21h             	;カレントディレクトリ変更

    mov	ax,4C00h
    int	21h             	;終了
FAIL:
    mov	ah,9
    mov	dx,offset ERR_MSG
    int	21h              	;エラーメッセージ表示

    mov	ax,4CFFh
    int	21h              	;終了

OP_MSG	db	'Input directory name : $'
ERR_MSG	db	0Dh,0Ah,'Fail to make the directory.',0Dh,0Ah,24h
BUFF	db	64,?,64 dup(?)
    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです


ディスク操作

6-1. ディスクのリセット

    MS-DOSではファイルバッファーが採用されています。ファイルバッファーとは、 ディスクキャッシュのごく単純な簡易版だと思ってください。これをリセットします。 すなわち、書き込まれていないデータは書き込まれ、読み出し用のデータをリフレッシュ します。

ディスクのリセット

    AH = 0Dh
    INT 21h

    戻り値：なし


ディスク操作

6-2. カレントディスクの取得

    カレントディレクトリは各ドライブごとに存在するので、カレントディスクの カレントディレクトリを取得する場合などにも使用します。

カレントディスクの取得

    AH = 19h
    INT 21h

    戻り値：AL = カレントドライブのドライブ番号

    ドライブ番号とは、Aドライブ=00、Bドライブ=01、Cドライブ=02・・・を表します。


ディスク操作

6-3. カレントディスクの変更

    カレントディスクを変更したい場合は、このシステムコールを使用します。

カレントディスクの変更

    AH = 0Eh
    INT 21h

    戻り値：AL = 論理ドライブ数

    戻り値であるALはMS-DOSのバージョンによって異なります。Ver.3.3では現在の変更後の カレントディスクが返ってきました。いつから変更されたかはわかりませんが、Ver.5.0以降は 論理ドライブ数が返ってきます。論理ドライブ数とは、CONFIG.SYSの[LASTDRIVE]で指定している ドライブ数で、実際のドライブ数とは異なります。

    このシステムコールは成功したかどうかがわかりません。前ページの カレントドライブの取得を用いてチェックしないと、カレントドライブの変更が成功したのか どうかわかりませんので、必要によってはチェックします。


ディスク操作

6-4. ドライブ情報の取得

    ドライブのクラスタ数、１クラスタあたりのセクタ数、１セクタあたりのバイト数を 得ることができます。

カレントドライブ情報の取得

    AH = 1Bh
    INT 21h

    戻り値：AL = １クラスタあたりのセクタ数
            CX = １セクタあたりのバイト数
            DX = １ドライブあたりのクラスタ数
            DS:BX = FAT-IDのアドレス

指定ドライブ情報の取得

    AH = 1Bh
    DL = ドライブ番号
    INT 21h

    戻り値：AL = １クラスタあたりのセクタ数
            CX = １セクタあたりのバイト数
            DX = １ドライブあたりのクラスタ数
            DS:BX = FAT-IDのアドレス

クラスタやセクタの説明は省略しますが、
ドライブの容量 = AL × CX × DX
が成り立つと言うことだけ覚えておいてください。

    ドライブ番号が無効なときは、AL=FFhが返されます。

    FAT-IDはその値からどのようなディスク(ドライブ)かがわかりますが、 主にフロッピーが主流だったときのもので、現在用いられているような多くのドライブ (FD,MO,PD,ZIP,HDD,スーパーディスク,DVD-ROM,CD-ROM,etc...)すべてを 区別することはできませんので、詳しいことは省略します。


ディスク操作

6-5. 空き容量取得

    ファイルを保存するときは、ディスク容量が足りるかどうかをあらかじめチェックしておくと、 プログラマーにとってもユーザーにとっても便利です。ここではドライブの空き容量を取得 するシステムコールを紹介します。

空き容量取得

    AH = 36h
    DL = ドライブ番号(0=カレントディスク、1=Aドライブ・・・)
    INT 21h

    戻り値：AX = １クラスタあたりのセクタ数
            BX = 使用可能なクラスタ数
            CX = １セクタあたりのバイト数
            DX = １ドライブあたりのクラスタ数

    AX = FFFFhの場合は、ドライブ番号の指定が無効なドライブ番号であったことを示します。

    前ページのシステムコール(AH=1Bh,1Ch)はあまり推奨されていません。なるべくこちらを 使用するようにしてください。ただし、MS-DOS Ver2.0以前ではサポートされていません。

ドライブの容量 | = AX × CX × DX
ドライブの空き容量 | = AX × CX × BX


ディスク操作

6-6. 練習問題

問題
    ドライブ番号を入力し、そのドライブのディスクの容量と、空き容量を表示する プログラムを作れ。ただし、無効なドライブ番号を入力されたら、カレントドライブの 情報を表示するものとする。


ディスク操作

6-6. 練習問題

問題
    ドライブ番号を入力し、そのドライブのディスクの容量と、空き容量を表示する プログラムを作れ。ただし、無効なドライブ番号を入力されたら、カレントドライブの 情報を表示するものとする。
解答
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .386
    ORG	100h

START:
    mov	ah,9
    mov	dx,offset OP_MSG
    int	21h               	;プロンプト表示

    mov	ah,01
    int	21h             	;1文字入力

    and	al,01Fh         	;↓ドライブ番号に変換
    cmp	al,1
    jae	short GETINFO
    cmp	al,1Ah
    jbe	short GETINFO
    mov	al,0            	;↑ここまで
GETINFO:
    mov	ah,36h
    mov	dl,al
    int	21h              	;情報の取得
    cmp	ax,-1
    jnz	DISPINFO
    mov	al,0
    jmp	short GETINFO   	;無効なドライブならカレントドライブ

DISPINFO:
    push	ax
    push	dx
    mov	ah,9
    mov	dx,offset MSG1
    int	21h             	;メッセージ表示
    pop	dx
    pop	ax

    xchg	cx,dx
    pusha
    xor	eax,eax
    xor	ebx,ebx
    xor	ecx,ecx
    xor	edx,edx
    popa                    	;EAX = １クラスタあたりのセクタ数
                                    ;EBX = 使用可能なクラスタ数
                                    ;ECX = １セクタあたりのバイト数
                                    ;EDX = １ドライブあたりのクラスタ数
    mul	edx             	;EDX:EAX = １クラスタあたりのバイト数
    push	eax
    mul	ecx              	;EDX:EAX = ドライブの全容量
    call	DISP            	;前容量表示

    pop	eax
    mul	ebx             	;EDX:EAX = 空き容量

    push	eax
    mov	ah,9
    mov	dx,offset MSG2
    int	21h             	;メッセージ表示
    pop	eax

    call	DISP            	;空き容量表示
    mov	ax,4C00h
    int	21h

DISP:                           	;↓EAXを10進数で表示するルーチン
    mov	ecx,10
    xor	edx,edx
    div	ecx
    push	edx
    or	eax,eax
    jz	DISPE
    call	DISP
DISPE:
    pop	edx
    add	dl,'0'
    mov	ah,2
    int	21h
    ret

OP_MSG	db	'Input drive : $'
MSG1	db	0Dh,0Ah,'Total Space  : $'
MSG2	db	0Dh,0Ah,' Free Space  : $'
BUFF	db	64,?,64 dup(?)
    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

C:\>DISKFREE
Input drive : A
Total Space  : 1457664
 Free Space  : 609792
C:\>
    ※このプログラムはi386未満のCPUでは動作しません。
    ※約2Gbyte以上では正確な値を表示しません。


ディスク操作

6-7. 交換可能ドライブの検査

    カレントドライブまたは指定したドライブが交換可能などライブかどうかを調べます。 交換可能なドライブとは、フロッピーやMOなどで、交換不可能なドライブとはハードディスク などを指します。

交換可能ドライブの検査

    AX = 4408h
    BL = ドライブ番号(0:カレントドライブ、1:Aドライブ、2:Bドライブ、・・・)
    INT 21h

    戻り値：CF = 0 のとき
                   AX = 0 : 交換可能ドライブ
                   AX = 1 : 交換不可能なドライブ
            CF = 1 のとき
                   AX = 0001 : ALが無効
                   AX = 000F : ドライブ番号(BL)が無効


ディスク操作

6-8. リトライ回数の設定

    MS-DOSはディスクアクセスに失敗した場合、デフォルトで3回リトライを行います。 それでも失敗した場合はユーザーにどうするか尋ねます。このシステムコールは 何回リトライするかを設定するものです。

リトライ回数の設定

    AX = 440Bh
    BX = リトライ回数
    CX = 待ち時間
    INT 21h

    戻り値：CF = 0 のとき
                   正常に実行された
            CF = 1 のとき
                   AX = 0001 : ALが無効

CXで与える待ち時間は、決まった単位を持っておらず、CPUの処理速度などに 依存します。


ディスク操作

6-9. ディスクの直接操作

    MS-DOSにはディスクから直接読み込んだり、書き込んだりするシステムコールも用意されて います。しかしこのシステムコールを用いることは推奨しません。なぜならば面倒なのと、 ひとつ間違えばディスクが壊れる からです。フロッピーだろうが、ハードディスクだろうが、容赦なく壊れます。

    逆に言えば、故意に壊すことも可能です。 故意に壊すプログラムを作成しないでください。

    フォルダーやファイルといった概念を捨てて、ディスクを丸ごと読み書きできるので、 フロッピーを他のフロッピーにコピーするプログラムなどの作成ができます。

ディスクからの直接読み込み

    AL = ドライブ番号(0:Aドライブ、1:Bドライブ、・・・)
    DS:BX = 読み込んだデータを格納するバッファのアドレス
    CX = 読み込むセクタ数
    DX = 読み込み開始セクタ番号
    INT 25h
    POPF

    戻り値：CF = 0 のとき
                   正常に実行された
            CF = 1 のとき
                   失敗

ディスクほの直接書き込み

    AL = ドライブ番号(0:Aドライブ、1:Bドライブ、・・・)
    DS:BX = 書き込むべきデータが格納されているバッファのアドレス
    CX = 書き込むセクタ数
    DX = 書き込む開始セクタ番号
    INT 26h
    POPF

    戻り値：CF = 0 のとき
                   正常に実行された
            CF = 1 のとき
                   失敗

  コール後はフラグがPUSHされたまま戻ってくるので、「POPF」する必要が あります。

    セグメント以外のレジスタはすべて破壊されます。

    ドライブ情報の取得を行うことで、丸ごとコピーしたい ディスクのセクタ数および1セクタあたりのバイト数がわかるので、何セクタ分コピーすれば よいのか計算で求めることができます。

    バッファはセグメントをまたがるようには指定できません。例えば、1セクタ1024バイト のディスクから1セクタ読み込むのに、BX=FF00hなどとすることはできません。


プロセス管理

7-1. プログラムの実行

    私はN88-BASICを使用しているときに、プログラムを作ってセーブしようとして、 「げっ！ディスクをフォーマットしていない！」と泣きを見たことが何度かあります。 現在のようにフォーマット済みディスクなんてのはなく、買ってきたディスクは フォーマットしないと使えず、フォーマットをするためには当然フォーマットプログラムを 起動しなければならないのですが、N88-BASIC時代は他のプログラムを起動するためには、 自分をメモリーから消さなければならないのです。つまり、フォーマットするためには、 今作ったプログラムを諦めなければならなかったのです。

    MS-WindowsのようなマルチタスクOSでは、そんなこと気にせずにフォーマットできます。 シングルタスクのMS-DOSでは、同時にプログラムを実行させることは不可能ですが、 メモリーの許す限り、プログラムから他のプログラムを実行させ、元のプログラムに 戻ると言うことが可能です。（他のプログラムをあたかもサブルーチンのように呼び出す ことができます。）

プログラムの実行

    AX = 4B00h
    DS:DX = プログラム名の格納アドレス
    ES:BX = パラメータブロックの格納アドレス
    INT 21h

    戻り値：CF = 0 のとき
                   正常に実行された
            CF = 1 のとき
                   AX = 0001h : ALの値が無効
                   AX = 0002h : ファイルが存在しない
                   AX = 0008h : メモリ不足
                   AX = 000Ah : 実行環境が32Kバイト以上であった
                   AX = 000Bh : EXEファイルのヘッダーが異常

    一般に、自分自身を親プロセス、呼び出して実行するプログラムを子プロセス （略して「子プロ」）と言います。

    親プロセスがCOMファイルの場合、自分が実行されるときに空きメモリーすべてを 使用します。したがって、メモリーを変更しないと 子プロセスを実行するメモリーが足りなくて実行できません。また、自分がEXEファイル の場合でも、可能な限りメモリーを子プロセスのためにあけておくべきです。

    子プロセスが終了するとCF=0で戻ってきます。

    また、親プロセスが開いているファイルは、ファイルポインタを何らかの形で、 子プロセスに渡すことで、引き続き子プロセスでも使用することができます。もちろん 子プロセスでそのファイルをクローズすれば、親プロセスで使用することはできなく なります。しかしこのような方法でファイルを他のプログラムでも使用するのは あまり好ましい使い方ではありません。


プロセス管理

パラメータブロック

    パラメータブロックとは、子プロセスに引き継ぐ環境の設定のことで、下記の14バイト からなります。
    環境変数領域のアドレスは、セグメントで指定します。この値が そのまま子プロセスのPSPの2Chに格納されます。0000hを指定すれば、自動的に親プロセス (自分自身)の環境変数をそのまま子プロセスでも使用します。

    PSPのFCBアドレスについては、FCB自体が昔のMS-DOSで使われていたシステムなので、 説明は省略します。普通は上図のように、ES:5Ch、ES:6Chを格納しておいてください。 (ESはPSPのセグメント)

    コマンドラインパラメータは、下図のように指定します。


プロセス管理

    このシステムコールは、環境変数「PATH」内を検索してくれません。したがって、「PATH」内を 検索したい場合は自分で環境変数「PATH」を参照して、目的のプログラムを検索しなくては なりません。しかしこれは結構面倒な作業です。そこで、COMMAN.COMの助けを借りるプログラマーが 多いようです。

    つまり、COMMAND.COMは「PATH」を勝手に検索してくれます。もしこれをプログラムで 検索しようとすると、
  1. PATHという環境変数を探す。
  2. その中から、AH=4Ehのシステムコールを使用して、プログラムを探す。
  3. 実行する
ということを行わなくてはならない。しかし、COMMAND.COMの助けを借りれば、 一番面倒な(2)の作業を省略でき、
  1. COMSPECという環境変数を探す。
  2. 実行する
ということになります。環境変数「COMSPEC」とは、通常COMMAND.COMの場所が格納されているので、 これを信用して、パラメータを指定して実行するだけでよくなる。

    また、COMMAND.COMは次の書式で、プログラムやDIRなどの内部コマンド、バッチファイルを 実行することができます。しかもプログラムの場合は、「PATH」から勝手に検索してくれます。 ただし、COMMAND.COMが実行される分だけ空きメモリーが必要となります。


プロセス管理

7-2. 練習問題

問題
    カレントドライブのカレントディレクトリと、ルートディレクトリで、「DIR」 コマンドを実行するプログラムを作れ。


プロセス管理

7-2. 練習問題

問題
    カレントドライブのカレントディレクトリと、ルートディレクトリで、「DIR」 コマンドを実行するプログラムを作れ。
解答
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	bx,offset _END  	;↓自分のメモリを確保するための計算
    mov	cl,4
    shr	bx,cl
    inc	bx              	;↑ここまで
    mov	sp,offset _END  	;SP設定
    mov	ah,4Ah
    int	21h             	;自分のメモリ確保

    xor	ax,ax           	;↓パラメータブロック設定
    mov	PARAM_BLOCK[00],ax
    mov	PARAM_BLOCK[02],offset CMD_LINE
    mov	PARAM_BLOCK[04],cs
    mov	PARAM_BLOCK[06],word ptr 5Ch
    mov	PARAM_BLOCK[08],es
    mov	PARAM_BLOCK[10],6Ch
    mov	PARAM_BLOCK[12],es	;↑ここまで

    push	word ptr [ds:2Ch]
    pop	es              	;ES <= 環境変数領域のセグメント

    call	SEARCH_COMSPEC  	;'COMSPEC'を探す
    or	di,di
    jnz	EXEC            	;DI=0 なら環境変数がない
    mov	ah,9
    mov	dx,offset ERR_MSG
    int	21h             	;エラーメッセージ表示
    mov	ax,4CFFh
    int	21h             	;異常修了
EXEC:
    push	es              	;↓COMMAND.COM実行の準備
    pop	ds
    push	cs
    pop	es
    mov	ax,4B00h
    mov	dx,di
    mov	bx,offset PARAM_BLOCK
    pusha
    push	ds
    push	es              	;↑ここまで
    int	21h             	;/C DIR 実行
    pop	es              	;↓次の準備
    pop	ds
    popa
    inc	byte ptr cs:CMD_LINE
    mov	cs:CMD_LINE2,byte ptr '\' ;引数'/C DIR \'に設定
                            ;↑ここまで
    int	21h             	;実行

    mov	ax,4C00h
    int	21h             	;終了

SEARCH_COMSPEC:
    mov	si,offset COMSPEC	;DS:SI <= 検索文字列(COMSPEC)
    xor	di,di           	;ES:DI <= 検索開始位置
SEARCH_COMSPEC2:
    cmp	[es:di],word ptr 0	;環境変数領域が終わりか？
    jz	short SEARCH_NON	;無かった
    mov	cx,8            	;'COMSPEC'の文字数
    push	si
    repe	cmpsb	        	;ds:si = 'COMSPEC'
                            ;es:di = 環境変数領域
    pop	si
    jnz	short SEARCH_COMSPEC2
SEARCH_END:
    ret                     	;見つかった
SEARCH_NON:
    xor	di,di
    ret                      	;見つからなかった

ERR_MSG 	db	'環境変数「COMSPEC」が設定されていません',0Dh,0Ah,24h
PARAM_BLOCK	dw	7 dup(?)
COMSPEC		db	'COMSPEC=',0
CMD_LINE	db	7,'/C DIR '
CMD_LINE2	db	0Dh,0Dh
            dw	30 dup(?)
_END:

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです
    ※このプログラムはi186未満のCPUでは動作しません。


プロセス管理

7-3. プログラム終了

    プログラムの終了は今までにも何度も出てきた。これから紹介するシステムコールは プログラムを終了し、使用していたメモリーを解放するものである。プログラムが 「AH = 48H」を使用して割り当てたメモリも 解放される。古いMS-DOSでは解放されないらしい。

    また、使用していたファイルはクローズされないため、もしクローズしないで プログラムを終了した場合は、完全にセーブできていなかったり、他のプログラムによって 悪戯されたりするので、必ずクローズしてから終了すべきである。

プログラム終了

    AH = 4Ch
    AL = リターンコード
    INT 21h

    戻り値：なし

    今までは
    mov	ax,4C00h
    int	21h

というように、ALレジスタに00とか、数字も入れてきた。ALレジスタは リターンコードというもので、詳細は後述する。

    また、Ver2.0以前のMS-DOSのCOMファイルでは他のシステムコールが用意されていた。

COMファイルのプログラム終了

    CS = PSPの先頭アドレス(セグメント)
    INT 20h

    戻り値：なし

プログラム終了

    AH = 00h
    INT 21h

    戻り値：なし

    上記の２つのシステムコールは、Ver2.0以前のもので、互換性を保つためそれ以降の MS-DOSでも使用できるが、使用すべきではない。特に前者は「INT　21H」 ではなくて「INT　20h」を使用しているところがポイントで、他のレジスタ は使用しない(CSは除く)ので、コンパイルしたプログラムが小さくなる。

    また、MS-DOSでは保証はしていないが、プログラムが実行された直後のSS:SP は0000Hを指しているため、単に「RET」命令を実行すると、CS:0000H にジャンプし、そこはPSPの先頭アドレスであり、そこには「INT　20H」という 命令が格納されているため、COMプログラムが終了されるという、ちょっと行儀が悪いが チップテクニックとして使用される。(推奨はしない)


プロセス管理

プログラムの常駐終了

    プログラムの常駐終了は以前に解説した。Ver2.0以前の MS-DOSと互換性を保つためのシステムコールも用意されているので、併せて紹介する。

常駐終了

    AH = 31h
    AL = 親プロセスに返す終了コード
    DX = メモリーに残すサイズ(バイト) ÷ 16
    INT 21h

    戻り値：なし

PSPの領域からDXで設定した分だけメモリーに残ります

常駐終了

    DX = メモリーに残すサイズ(バイト)
    INT 27h

    戻り値：なし

    前者の方は前に解説したので、ここでは省略する。後者のシステムコールは MS-DOS Ver.2.0以前のものと互換性を保つために用意されているもので、それ以降の MS-DOSでは使用すべきではない。「INT　27H」が呼び出されると、 CS:0000HからCS:DXまでがメモリーに残り、プログラムを 終了する。もちろんファイルなどはクローズされない。

おまけ

    プログラムの終了ではないが、コンピュータをリセットする方法を紹介する。 これはMS-DOSのシステムコールなどではなく、他のOSでも80x86系のCPUを用いている コンピュータならば動作する機能である。

    80x86系のCPUは電源を入れた直後や、リセットボタン(最近のパソコンにはないが)を 押すと、FFFFh:0000hに制御が移る。したがって、強制的にここにジャンプすれば、 リセットボタンが押されたのと同様の処理がなされる。


プロセス管理

7-4. リターンコード

    リターンコードとは、C言語で言うmain()関数の戻り値のことで、MS-DOSでは 1バイトの数字である。これは親プロセス(MS-DOSのコマンドラインから実行された場合は MS-DOS)に渡されて処理される。

    例えば、バッチファイルの中でプログラムを実行し、エラーならばバッチファイルを 終了して、正常にプログラムが実行されたら次のプログラムを実行するというような感じで 使用されたり、プログラムから「AX=4B00hのシステムコール」 で子プロセスを実行し、その成功の可否を判断して次の処理を行うなどの 用途で使用する。

    通常、プログラムが正常に終了すれば、リターンコードは0、失敗した場合は その理由によってプログラマーが決めた値を返す。

    バッチファイルでプログラムのリターンコードを得るには「IF ～ ERRORLEVEL」 を用いる。例えば、下記のようにすると、

  C:\>IF /?
  バッチファイル中で条件処理を実行します.

  IF [NOT] ERRORLEVEL 番号 ｺﾏﾝﾄﾞ
  IF [NOT] 文字列1==文字列2 ｺﾏﾝﾄﾞ
  IF [NOT] EXIST ﾌｧｲﾙ名 ｺﾏﾝﾄﾞ

    NOT               条件が偽のときだけコマンドを実行するように指定します.
    ERRORLEVEL 番号   最近実行したプログラムが返した終了コードが指定した番号
                      以上であれば真になるように指定します.
    ｺﾏﾝﾄﾞ　           条件が真であれば実行するコマンドを指定します.
    文字列1==文字列2  文字列が一致すれば真になるように指定します.
    EXIST ﾌｧｲﾙ名      ファイル名に指定されたファイルが実際に存在すれば真に
                      なるように指定します.

  C:\>TYPE TEST.BAT
  TEST.COM
  IF ERRORLEVEL 5 ECHO リターンコードは５以上です

  C:\>
TEST.COMのリターンコードが５以上ならばメッセージを表示し、それ未満 なら何も表示しないバッチファイルになります。

    プログラムから子プロセスを呼び出し、子プロセスからのリターンコードを調べるには 下記のシステムコールを使用します。

子プロセスのリターンコード

    AH = 4Dh
    INT 21h

    戻り値：AL : 子プロセスからのリターンコード
            AH : 子プロセスの終了状態
                 AH = 0 : 通常終了
                 AH = 1 : CTRL + C による中断
                 AH = 2 : 致命的なエラー
                 AH = 3 : 常駐終了


プロセス管理

例題

    DIRコマンドを実行してカレントディレクトリを表示し、処理を継続するかどうかを訪ねる。 続いて、キー入力を行い、[y]もしくはリターンキーが押されたら リターンコード1を、それ以外が押されたら、0を返すプログラムを実行し、 リターンコードが1ならば、カレントドライブのルートディレクトリのファイルを表示(DIR \)する。 リターンコードが1でなければ、そこで処理を中止するプログラムを作る。

    ただし、黒文字の部分はバッチファイル、緑文字部分は アセンブラで書く。

;
;	YESNO.ASM → YESNO.COM
;
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:
    mov	ah,1
    int	21h

    cmp	al,'Y'
    jz	YES
    cmp	al,'y'
    jz	YES
    cmp	al,0Dh
    jz	YES

    mov	ax,4C01h
    int	21h

YES:
    mov	ax,4C00h
    int	21h

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

ECHO OFF
DIR
ECHO :
ECHO 処理を続行しますか？
ECHO :
YESNO
IF ERRORLEVEL 1 DIR \

A:\YESNO>TEST			←バッチファイルを実行

 ドライブ A: のボリュームラベルはありません.
 ボリュームシリアル番号は 0F1E-1ED7
 ディレクトリは A:\YESNO

.            <DIR>         00-01-02   17:32
..           <DIR>         00-01-02   17:32
YESNO    COM            26 00-01-02   17:28
TEST     BAT           119 00-01-02   22:11
        4 個                 145 バイトのファイルがあります
                     423,018,496 バイトの空きがあります
:
処理を続行しますか？
:
y				←「y」と入力
 ドライブ A: のボリュームラベルはありません.
 ボリュームシリアル番号は 0F1E-1ED7
 ディレクトリは A:\

COMMAND  COM        54,988 94-12-08    6:20
BOOT         <DIR>         94-08-29   16:24
AUTOEXEC BAT           783 99-07-07    1:15
CONFIG   SYS           905 99-07-07    1:15
YESNO        <DIR>         00-01-02   17:32
        5 個              56,676 バイトのファイルがあります
                     423,018,496 バイトの空きがあります
A:\YESNO>
A:\YESNO>TEST			←もう一度実行

 ドライブ A: のボリュームラベルはありません.
 ボリュームシリアル番号は 0F1E-1ED7
 ディレクトリは A:\YESNO

.            <DIR>         00-01-02   17:32
..           <DIR>         00-01-02   17:32
YESNO    COM            26 00-01-02   17:28
TEST     BAT           119 00-01-02   22:11
        4 個                 145 バイトのファイルがあります
                     423,018,496 バイトの空きがあります
:
処理を続行しますか？
:
n				←今度は「n」と入力

A:\YESNO>


プロセス管理

7-5. PSPアドレスの取得・設定

    PSPとは、プログラム・セグメント・プリフィックスの略である。MS-DOSがプログラムを ロードしたときに100Hバイトの作業領域を確保する。これがPSPである。PSPの内容は下記の通り。
オフセットアドレス サイズ
(Byte) 		内　容
00h～01h 2 INT　20hという命令
(ここにジャンプすれば、プログラムが終了する)
02h～03h | 2 | 使用可能な最初のMCBCOMファイルの場合、起動したときにはすべての メモリーがそのCOMファイルに割り当てられるので、9FFFhになっている。
04h～04h | 1 | MS-DOS予約
05h～09h 4 CALL　0005hという命令
CP/MというOSとの互換用
MS-DOSでは使用しない
0Ah～0Dh | 4 | このプログラムが終了したとき、ここに格納されているアドレスにINT命令によってジャンプする
0Eh～11h | 4 | CTRL+Cが押されたとき、ここに格納されているアドレスにINT命令によってジャンプする
12h～15h | 4 | 致命的ディスクエラーが発生したとき、ここに格納されているアドレスにINT命令によってジャンプする
16h～17h | 2 | 親プロセスのPSP
18h～2Bh | 20 | ファイルテーブル(解説は省略)
2Ch～2Dh | 2 | 環境変数領域のセグメント
2Eh～2Fh | 2 | SPの待避領域(MS-DOSが使用)
30h～31h | 4 | SSの待避領域(MS-DOSが使用)
32h～4Fh | 30 | MS-DOS予約領域
50h～52h 3 INT　21h、RETFという命令がおかれる。
システムコール実行時にここをCALL　farしても良いが、あまり使われない。
53h～54h | 2 | MS-DOS予約領域
55h～5Bh | 7 | 拡張FCB。説明は省略。
5Ch～6Bh | 16 | 第一FCB。説明は省略
6Ch～7Fh | 20 | 第二FCB。説明は省略
80h～80h | 1 | コマンドラインからの引数の文字数
81h～FFh
コマンドラインからの引数。一番最後に0Dhが入っている。

PSPアドレスの取得(MS-DOS Ver3.10以降)

    AH = 62h
    INT 21h

    戻り値：BX = 現在実行されているプログラムのPSPのセグメントアドレス

PSPアドレスの取得(MS-DOS Ver2.11以降、MS-DOS非公開)

    AH = 51h
    INT 21h

    戻り値：BX = 現在実行されているプログラムのPSPのセグメントアドレス

PSPアドレスの設定(MS-DOS非公開)

    AH = 50h
    BX = 新しいPSPのセグメントアドレス
    INT 21h

    戻り値：なし

PSPアドレスの複製(MS-DOS非公開)

    AH = 55h
    DX = 複写先のセグメントアドレス
    INT 21h

    戻り値：なし
    動　作：現在実行中のプログラムのPSPを複製する


プロセス管理

7-6. DTAアドレスの取得・設定

    DTAとはディスク・トランスファー・アドレスの略である。FCB(ファイル・コントロール・ ブロック)とともに昔のMS-DOSで使用されていた。これらはMS-DOSが開発される前に広く利用されて いたCP/MというOSとの互換性を保つために作られたものである。したがって、ディレクトリという 概念も存在せず、ほとんどMS-DOSでは使用されていないので、ここでも説明は省略する。

    ただ唯一DTAの名残が残っているのが「ファイル検索」 というシステムコールである。これはDTA領域にファイル名を取得するので、標準の領域に 格納されると困る場合に、以下に説明するシステムコールを用いて変更することができる。

DTAアドレスの取得

    AH = 2Fh
    INT 21h

    戻り値：ES:BX = DTAアドレス

DTAアドレスの設定

    AH = 1Ah
    DS:DX = 新しいDTAアドレス
    INT 21h

    戻り値：なし


プロセス管理

7-7. プログラムの実行2(MS-DOS非公開)

    先ほどの練習問題では、わざわざCOMMAND.COMを 呼び出して実行しました。先ほどの例でもおわかりの通り、結構面倒な処理でした。

    また、環境変数を操作する「SET」コマンドは、COMMAND.COMを 呼び出して処理しても、実行したCOMMAND.COM用の環境変数を書き換え、それが 終了すると、書き換えた環境変数も解放されてしまうため、結果として環境変数は 書き換えられていないという事態が起こります。

    そこで、MS-DOS非公開のシステムコールを用いると、常駐しているCOMMAND.COM が直接処理してくれるため、呼び出すのも簡単ですし、環境変数の設定もきちんと処理してくれます。 さらにディスクからCOMMAND.COMを呼び出す処理もないため、実行速度も 速くなりますし、メモリーの節約にもなります。

    ただし、呼び出す前にメモリーを開けておかないと、コマンドを実行できないばかりで なく、COMMAND.COMがエラーを起こします。その後どうなるかは、MS-DOSの バージョンにより異なるようです。

    また、このシステムコールは当然のことながら、「DIR」のような 内部コマンド、「FORMAT.EXE」のような外部コマンド、バッチファイルも 処理してくれ、「AX=4B00hのシステムコール」 と違い、PATHも処理してくれるのでお手軽です。なお、外部コマンド、 バッチファイルの場合も、「/C」を書く必要はなく、MS-DOSのコマンド ラインから入力するイメージのまま記入するだけです。

コマンドの実行(MS-DOS非公開)

    DS:SI = コマンド文字列格納アドレス
    INT 2Eh

    戻り値：AL = リターンコード
            AL,CS以外のレジスタは(SP,SSを含めて)全て破壊されている

    DS:SIはコマンド文字列格納アドレスです。以下のようなフォーマットで指定します。

CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:
    mov	si,offset CMD
    int	2Eh      	;DIR実行

    mov	ax,4C00h
    int	21h       	;プログラム終了

CMD	db	3,'DIR',0Dh

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

メモリーが足りなかった場合の例

  C:\>INT2E.COM

  メモリ アロケーション エラーです.
  COMMAND を起動できません. 終了します.

  C:\>
    上の例では御大層に「終了します」なんて表示してくれていますが、実は終了せずに きちんとプログラムに戻ってきています。そして、その後の 「AH=4Chのシステムコール」で、終了しています。

    このように正常に実行されなかったとしても、実行されたかどうかをチェックする 手段はありません。(CFでのチェックもできません。)　メモリーは多めに確保してあげましょう。

    ちなみに正常に動作するようにするには、
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:
    mov	bx,offset _END  	;↓自分のメモリを確保するための計算
    mov	cl,4
    shr	bx,cl
    inc	bx              	;↑ここまで
    mov	sp,offset _END  	;SP設定
    mov	ah,4Ah
    int	21h             	;自分のメモリ確保

    mov	si,offset CMD
    int	2Eh             	;DIR実行

    mov	ax,4C00h
    int	21h             	;プログラム終了

CMD	db	3,'DIR',0Dh
    dw	30 dup(?)
_END:

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです


プロセス管理

7-8. 練習問題2

問題
    プロンプトを表示し、コマンドを入力させる。入力されたコマンドを 実行し、「exit」と入力されるまで繰り返すプログラムを作れ。イメージ的には、 MS-DOSのコマンドプロンプトと同じようなものである。


プロセス管理

7-8. 練習問題2

問題
    プロンプトを表示し、コマンドを入力させる。入力されたコマンドを 実行し、「exit」と入力されるまで繰り返すプログラムを作れ。イメージ的には、 MS-DOSのコマンドプロンプトと同じようなものである。
解答
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    ORG	100h

START:
    mov	bx,offset _END  	;↓自分のメモリを確保するための計算
    mov	cl,4
    shr	bx,cl
    inc	bx              	;↑ここまで
    mov	sp,offset _END  	;SP設定
    mov	ah,4Ah
    int	21h             	;自分のメモリ確保

    mov	ah,9
    mov	dx,offset MSG1
    int	21h             	;メッセージ表示

MAIN:
    mov	ah,19h          	;↓プロンプト作成
    int	21h             	;　カレントドライブ
    add	al,'A'
    mov	PROMPT,al

    mov	ah,47h
    mov	dl,0
    mov	si,offset PROMPT +3
    int	21h             	;　カレントディレクトリ

    mov	di,si
    mov	cx,64
    mov	al,0
    repnz	scasb
    mov	[di-1],byte ptr '>'
    mov	[di]  ,byte ptr '$';↑プロンプト作成

    mov	ah,9
    mov	dx,offset PROMPT
    int	21h             	;プロンプト表示

    mov	ah,0Ah
    mov	dx,offset BUF
    int	21h             	;文字列入力

    mov	cx,4            	;↓入力文字が'exit'か？
    mov	si,offset BUF + 2
    mov	di,offset _EXIT
CHK_EXIT:
    lodsb
    and	al,0DFh         	;　大文字に変換
    cmp	[di],al
    jnz	CHK_END
    inc	di
    loop	CHK_EXIT
    lodsb
    cmp	al,20h
    ja	CHK_END
    jmp	short BYEBYE       	;↑ここまで
CHK_END:

    mov	ah,6
    mov	dl,0Dh
    int	21h
    mov	dl,0Ah
    int	21h             	;改行

    mov	_SS,ss
    mov	_SP,sp
    mov	si,offset BUF +1
    int	2Eh
    cli 	                	;割り込み不可
    push	cs
    pop	ds
    push	cs
    pop	es
    mov	ss,_SS
    mov	sp,_SP
    sti	                	;割り込み許可

    mov	ah,6
    mov	dl,0Dh
    int	21h
    mov	dl,0Ah
    int	21h             	;改行

    jmp	short MAIN

BYEBYE:
    mov	ah,9
    mov	dx,offset MSG2
    int	21h

    mov	ax,4C00h
    int	21h

INT23:		                	;新しいCTRL +C の処理
    iret

MSG1	db	0Dh,0Ah,'いらっしゃいませ。。。',0Dh,0Ah,24h
MSG2	db	0Dh,0Ah,0Ah,'さようなら。。。',0Dh,0Ah,24h
_EXIT	db	'EXIT'
PROMPT	db	?,':\',64 dup(?)
BUF	db	100,100 dup(?)
_SS	dw	?
_SP	dw	?
    dw	30 dup(?)
_END:

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

C:\>ESHELL			←実行

いらっしゃいませ。。。
C:\>COPY ESHELL.ASM A:		←「COPY」実行
        1 個のファイルをコピーしました.

C:\>CD WINDOWS			←「CD」実行

C:\WINDOWS>
C:\WINDOWS>EXIT			←終了

さようなら。。。

C:\WINDOWS>


その他のシステムコール

8-1. 日付の取得・設定

    コンピュータの時計から日付を取得したり、設定したりします。

日付の取得

    AH = 2Ah
    INT 21h

    戻り値：AL = 曜日(0=日、1=月、・・・、6=土)
            CX = 年(1980～2079)
            DH = 月(1～12)
            DL = 日(1～31)

日付の設定

    AH = 2Bh
    CX = 年(1980～2079)
    DH = 月(1～12)
    DL = 日(1～31)
    INT 21h

    戻り値：AL = 00h 正常に実行された
            AL = FFh 無効な日付


その他のシステムコール

8-2. 時間の取得・設定

    コンピュータの時計から時間を取得したり、設定したりします。

時間の取得

    AH = 2Ch
    INT 21h

    戻り値：CH = 時(0～23)
            CL = 分(0～59)
            DH = 秒(0～59)

時間の設定

    AH = 2Dh
    CH = 時(0～23)
    CL = 分(0～59)
    DH = 秒(0～59)
    DL = 0(固定)
    INT 21h

    戻り値：AL = 00h 正常に実行された
            AL = FFh 無効な時間


その他のシステムコール

8-3. 割り込みベクタの設定・取得

    割り込みベクタの設定や取得は、システムコールを使用しなくても可能です。 しかし、割り込みを禁止(CLI命令)にし割り込みベクタを計算して、割り込み許可(STI命令)に すると結構面倒な処理が必要になる。(割り込みベクタについては こちらを参照)さらに、この処理は一歩間違えると暴走したりするようになる 危険な処理であるためにシステムコールとして用意されている。したがってできる限り システムコールを用いて行うべきである

割り込みベクタの取得

    AH = 35h
    AL = 割り込み番号
    INT 21h

    戻り値：ES:BX = 割り込み時の処理ルーチンのアドレス

割り込みベクタの設定

    AH = 25h
    AL = 割り込み番号
    DS:DX = 割り込み時の処理ルーチンのアドレス
    INT 21h

    戻り値：なし

    サンプルプログラムはこちらを参照


その他のシステムコール

8-4. ベリファイフラグの設定

    ベリファイフラグとは、ディスクへの書き込みが正しく行われたかどうかをチェックする フラグです。現在のフロッピーやハードディスクなどストレージデバイスは性能が上がっており、 ドライブ自体が、正常に書き込まれたかどうかをチェックし、正常に書き込まれていなければ 何度かやり直すようになっているようです。しかし、昔のフロッピーはドライブ自体に このようなチェック機構が内蔵されておらず、ディスク自体も耐久性がなかったために、 OSでチェックする必要がありました。

    当然この機能をONにすれば、確実に書き込めるようになりますが、処理速度は極端に 遅くなります。しかも上述のように現在ではドライブの性能が上がっていますので、 通常はOFFでかまわないでしょう。

    MS-DOSではプロンプトから「VERIFY」と入力すれば、現在このフラグが どうなっているのかを調べることができます。

  C:\>VERIFY		←ベリファイフラグをチェック
  VERIFY は OFF		←ベリファイフラグはOFFだった

  C:\>VERIFY ON		←ベリファイフラグをON

  C:\>VERIFY		←ベリファイフラグをチェック
  VERIFY は ON		←ベリファイフラグはONだった

  C:\>
    このベリファイフラグをプログラム上で変更するには

ベリファイフラグの設定

    AH = 2Eh
    AL = 0 : ベリファイを行わない(OFF)
         1 : ベリファイを行う(ON)
    DL = 0 (固定)
    INT 21h

    戻り値：なし


その他のシステムコール

8-5. MS-DOSのバージョン取得

    MS-DOSには各バージョンがあり、各バージョンによって、システムコールや その他の仕様が異なります。したがってバージョンによって異なるシステムコールなどを 使うプログラムは、バージョンをチェックして今動いているMS-DOSで処理を続けて 大丈夫なのかを判断しなければならない場合があります。

    MS-DOSのバージョンは、例えばVer.3.30というようにメジャーバージョン(整数部=3)と、 マイナーバージョン(小数部30)の２つに大別されています。

MS-DOSのバージョン取得

    AH = 30h
    INT 21h

    戻り値：AL = メジャーバージョン
            AH = マイナーバージョン
            BH = OEMのシリアル番号
        BL:CX = 24bitのユーザー番号

    例えばVer.3.30ならば、AL=03h、AH=30hが返ってきます。また、Windows98でこの システムコールを行うとAL=07h、AH=0Ahでした。おそらくサービスパックなどに より異なるかと思います。

    OEMのシリアル番号とは、どこのメーカーのMS-DOSかを示します。MS-DOSは本来、 マイクロソフトが開発していますが、ハードウェアーと密接な関係のある部分は ハードウェアー会社が開発します。つまりマイクロソフトはOEMとしてハードウェアー 会社に供給し、ハードウェアー会社が自分のマシン用にセットアップして、ユーザーに 供給しているのです。特にNECのPC-9801、PC-9821シリーズと、DOS/VとかAT互換機は ハードウェアー的に仕様が大幅に異なっているのは有名な話です。

    したがってPC-9801用とDOS/V用とが別々に存在するソフトもあります。NECが 開発したMS-DOS(含MS-Windows)は、このOEMのシリアル番号がFFhになっていますので、 PC-9801かどうかを調べるためには、このOEMシリアル番号をチェックすればよいことに なります。


その他のシステムコール

8-6. 国別情報の取得・設定

    国別情報とは日付表示のフォーマットや、通貨記号などの情報です。

国別情報の取得

    AH = 38h
    AL = 国コード
    DS:DX = 国情報テーブルのアドレス
    INT 21h

    戻り値：CF = 0 : 正常に実行された
               = 1 : エラー

現在国の設定

    AH = 38h
    AL = 国コード
    DX = FFFFh(固定)
    INT 21h

    戻り値：CF = 0 : 正常に実行された
               = 1 : エラー

    国コードとは、各国に設定された番号で、00h=現在の国、01h=USA、・・・、51h=日本、・・・ となっています。

    国情報テーブルは下表の通りです。
オフセットアドレス | サイズ | 情　報 | 説　明 | USA設定
00h～01h 1Word 日付表示フォーマット 0:USA規格(m/d/y)
1:ヨーロッパ規格(d/m/y)
2:日本規格(y/m/d) 		0000h
02h～06h 5Byte 通貨記号 ASCII文字 '$'
00
00
00
00
07h～08h 2Byte 数字3桁ごとの区切り ASCII文字 ','
00
09h～0Ah 2Byte 小数点記号 ASCII文字 '.'
00
0Bh～0Ch 2Byte 日付分離記号 ASCII文字 '-'
00
0Dh～0Eh 2Byte 時刻分離記号 ASCII文字 '.'
00
0Fh～0Fh 1Byte 通貨記号ビット 0:通貨記号は数字の前に置き、スペースを入れない
1:通貨記号は数字の前に置き、スペースを入れる
2:通貨記号は数字の後に置き、スペースを入れない
3:通貨記号は数字の後に置き、スペースを入れる 		00
10h～10h | 1Byte | 通貨桁 | 通貨の小数点以下の桁数 | 02
11h～11h 1Byte 時刻フォーマット 0:12時制
1:24時制 		00
12h～15h 4Byte ケースマッピング
コールアドレス 		アスキーコード80h～FFhの文字を処理してくれるサブルーチンのアドレス(このアドレスをFAR CALLする) 省略
16h～17h 2Byte データリスト分離記号 ASCII文字 ','
00
18h～1Fh 8Byte 未定義


その他のシステムコール

8-7. CTRL + C の検査

    MS-DOSのプログラムを実行中に、暴走したり、すぐにプログラムを終了したいときは [CTRL]を押しながら[C]を押すと、プログラムが終了する 場合があります。(NEC、PC-9801、9821シリーズは[STOP]キーも同じ)

    これはMS-DOSがCTRL+Cをチェックしており、押された場合は特殊な処理を行います。 その特別な処理とは通常プログラムを終了するという処理だからです。MS-DOSでは 通常AH=01h～0Chのシステムコールを実行しているときのみ、CTRL+Cのチェックを行います。 このシステムコールを用いると、他のシステムコール中もCTRL+Cのチェックを行うかどうか 設定できます。

CTRL + C の検査フラグの取得

    AX = 3300h
    DL = 0 : チェックしない(OFF)
       = 1 : チェックする(ON)
    INT 21h

    戻り値：AL = FFh : ALの値が無効
            AL = FFh以外 : 正常に実行された

CTRL + C の検査フラグの設定

    AX = 3301h
    INT 21h

    戻り値：AL = FFh : DLの値が無効
            AL = FFh以外 :
                 DL = 0 : チェックしない(OFF)
                    = 1 : チェックする(ON)


その他のシステムコール

8-8. CTRL + C の処理

    例えば、ファイルの内容を表示するようなプログラム中にCTRL + Cを押すと、 終了してしまいます。しかしファイルをクローズしないで勝手に終わってしまっては 困ります。前ページのシステムコールでは、標準入出力中の チェックの有無は設定できません。

    このような場合は、プログラマーがCTRL + Cが押されたときの処理を自分で 書くことができます。自分が作って処理ルーチンの中でCTRL + Cが押されたという フラグをセットし、リターンします。そして本編のプログラム内で時々そのフラグを チェックし、フラグがセットされていれば、終了処理後終了させます。

    または、CTRL + Cが押された処理は何もしないようにすれば、CTRL + Cを 無視することができます。

    MS-DOSではCTRL + Cが押された場合、23h番の割り込み が発生します。そこで、この割り込みベクタを書き換えることで、] CTRL + Cの処理を書き換えることが可能となります。

    なお、NECのPC-9801、9821シリーズには[STOP]キーがあります。 MS-DOSではCTRL + Cと同様の働きをしますが、こちらは割り込み番号06h番の割り込みが 発生します。


EMS

9-1.EMSとは

    MS-DOSでは1MBytesのメモリー空間しかアクセスできません。ところがソフトによっては、 それ以上のメモリーを使いたい場合があります。そこで考え出されたのがEMSです。

    MS-DOSではユーザーが使えるメモリーは640KBytesで、それ以降1MBytesまでの空間は VRAMやROMなどが割り当てられています。そこで、CPUが管理するメモリーとは別に メモリーを増設し、それを640KBytes以降のROM空間に割り当てます。しかしROM空間 といえども小さい領域なので、これを切り替えて使おうというわけです。これは ノートにたとえられます。つまり、人間はノートの１ページしか見ることができません。 しかしページを切り替えることで、１ページ以上の紙面にメモしたり、読んだりすることが 可能になるわけです。

    上図のように、EMSの論理ページをコンピュータ上の物理ページに割り当てます。 するとこのページを通して、EMSのメモリーにアクセスすることができます。CPUは 特に、EMSにアクセスしているとは意識しません。そして、別の論理ページにページを めくると、また新たなページにアクセスすることができるようになります。この ページは16KBytesの大きさで、変えることはできません。また、EMSのバージョンが 4.0未満は、物理ページは4ページ連続していなければなりませんが、4.0以降は、 離れたところに論理ページをとることもできます。

    物理ページを実際のどの部分に置くかは、コンピュータの種類、拡張の仕方などに よって異なります。なぜならばROMがどの部分にあるかによって、物理ページをどこに 置くことができるかが変わってくるからです。

    なお、物理ページの場所のことをページフレームと呼びます。例えば、「ページフレーム はC0000h番地から」などのように使います。

    このようなEMSをハードウェアーEMSといいます。80286以降は、特別なEMSメモリーを 増設しなくても、CPUが1MBytes以上のメモリーを操作できますので、これをEMSのように 使うこともできます。これを仮想EMSといいます。ハードウェアーEMSは、EMSのために作られた メモリーなのでEMSとしてしか使用できませんが、仮想EMSはXMSなど他の用途にも使用できます。

    どちらのEMSも使い方の仕様は同じなので、ドライバーのバージョンが同じならば、 プログラマーは意識せずに使用できます。


EMS

9-2.EMSを使用するには

    EMSを使用するためには、ドライバーを組み込まなくてはなりません。当たり前ですが。 ドライバーの組み込み方は、このホームページの範囲外なので、説明書でも読んでください。

    アセンブラでEMSを使うためには次の手順を踏まなくてはなりません。
  • EMM(EMSドライバ)が組み込まれているかどうか調べる(ノートがあるか調べる)
  • EMSメモリーの空き容量を調べる(ノートのページが余っているか調べる)
  • EMSメモリーを割り当てる(空きページを使うよう宣言する)
  • ページフレームの場所を調べる
  • EMSメモリーを物理ページにマップする(ノートのページをめくる)
  • メモリーにアクセス(ノートにメモする、読む)
  • EMSメモリーを物理ページからアンマップする(ノートを閉じる)
  • EMSメモリーを解放する(ノートのページを消しゴムで消す)
青字の部分は、必要に応じて繰り返す
    MS-DOSのシステムコールは、「INT　21h」を呼び出して使用していましたが、 EMMのシステムコールは「INT　67h」を使用します。

    EMSを使用するにあたり、注意事項をあげます。

EMS領域をスタックとして使用しない
    EMSをスタックとして使用している最中に、EMSを使用する常駐プログラムやデバイスドライバー に制御が移った場合、そのプログラムがスタックを使用する可能性がある。このような場合、 EMSを使用している常駐プログラムなどが、自分自身あるいは自分用のデータを 破壊してしまう可能性がある。

常駐プログラムなどでEMSを使用する場合
    EMSを使用する常駐プログラムを作成する場合、EMSを使用する前にページが マップされているかどうか調べ、マップされていたらどのページがマップされているか 調べ、常駐プログラムから本来のプログラムに戻る際に、もとのページをマップ しなおさなければならない。つまりだれかがノートを開いていたら、自分が 使う前に、しおりを挟み、自分が使い終わったときに、もとのページを開いて おかないといけない。

    では、次ページからEMSのファンクションの説明をします。EMSのファンクションには、 OSのみが使用可能なファンクションも多く設けられています。これらはプログラムを 作るだけでは関係ないし、またEMMがプログラムからの呼び出しだと判断した場合には、 何もしない場合もありますし、OSが壊れる場合もありますので、省略します。OSを 作ろうというならば必要になってくるかもしれませんが、そのような考えを持っている 人は、こんなページを見るわけないでしょうから・・・。

    なお、EMSの規格はVersion 4.0のものです。このページで紹介するような基本的な ファンクションは3.2でもほとんど変わっていませんので、省略します。


EMS

9-3.EMSが使用可能か?

    EMSが仕様かどうかをチェックするためのファンクションは、

EMSステータスの取得

    AH = 40h
    INT 67h

    戻り値：AH = 00h ・・・ EMSが使用可能
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能

    というのがあります。つまり、このファンクションを呼び出して、AH=0ならば EMSが使用可能と言うことです。

    EMSはMS-DOS Ver5.0から標準で使用可能になりました。それ以前ではEMSを作っている メーカーが独自でサポートしていただけなので、いきなり、「INT　67h」を 呼び出すと、暴走する危険がありました。そこで、MS-DOS 5.0よりも前のMS-DOSで使用する 可能性のあるプログラムを作る場合には、EMMが組み込まれているか(「INT　67h」 を呼び出しても良いか)をまず調べる必要があります。

    そのためにはまず、「INT　67h」の割り込みベクターを調べます。そして、その セグメントのオフセット0Ah番地から"EMMXXXX0"という8文字があれば、EMMが組み込まれて いるということになります。したがって、EMSが使用可能かどうかを調べるためには、 以下のように書きます。
EMS_GET_STATUS:
    pusha
    push	es
    push	ds

    mov	ax,3567h
    int	21h			;get vector
    mov	di,0ah
    mov	cx,8
    mov	si,offset EMS_MEM
    push	cs
    pop	ds
    repz	cmpsb
    jcxz	@@
@@ERR:
    pop	ds
    pop	es
    popa
    stc
    ret				;使えない
@@:
    mov	ah,40h
    int	67h			;get ems status
    or	ah,ah
    jnz	@@ERR
    pop	ds
    pop	es
    popa
    clc
    ret				;使える
EMS_MEM	db	'EMMXXXX0'

EMSが使用可能かを調べる。
使用可能ならばCF=0、使用不可ならCF=1で帰る。


EMS

9-4.EMSのバージョン取得

    このファンクションは、EMM(ドライバー)のバージョンではなくて、EMS規格の バージョンを取得します。有名なものに、3.2と4.0があります。前に述べた、 物理ページをばらばらにとれるか、とれないか以外は、あまり大きな変更はありません。

EMSのバージョン取得

    AH = 46h
    INT 67h

    戻り値：AH = 00h ・・・ ALにバージョンが格納された
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能

    ALの上位4bitがメジャーバージョン、下位4bitがマイナーバージョンを表します。 つまり、AX=0040hならばEMS4.0、AX=0032hならばEMS3.2となります。
EMS_GET_VER:
    mov	ah,46h
    int	67h
    or	ah,ah
    jnz	@@ERR
    clc
    ret
@@ERR:
    stc
    ret

EMSのバージョンをALに取得する。
エラーなら、CFがセットされる。


EMS

9-5.ページフレームのアドレス取得

    EMSが使用可能で、EMSのバージョンもわかったら、ページフレームがどこなのかを 取得します。

ページフレームのアドレス取得

    AH = 41h
    INT 67h

    戻り値：AH = 00h ・・・ BXにページフレームのセグメントアドレスを格納した
                            BX = ページフレームのセグメントアドレス
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能

EMS_GET_PAGEFRAME:
    push	ax
    push	bx
    mov	ah,41h
    int	67h
    or	ah,ah
    jnz	@@ERR
    mov	es,bx
    pop	bx
    pop	ax
    clc
    ret
@@ERR:
    pop	bx
    pop	ax
    stc
    ret

ページフレームをESにセットする。
エラーならばCF=1で返す。


EMS

9-6.未使用ページ数の取得

    EMSを使用するためには、あとどのくらいのページが使用可能かを調べる必要があります。

未使用ページ数の取得

    AH = 42h
    INT 67h

    戻り値：AH = 00h ・・・ 正常実行
                            BX = 未使用ページ数
                            DX = 総ページ数
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能

    1ページの大きさは16KBytesなので、ページ数がわかれば、どのくらい使用できるのか 計算できます。


EMS

9-7. アロケート

    さて、いよいよEMSを使います。まずEMS領域を割り当てなくてはいけません。

アロケート

    AH = 43h
    BX = 割り当てたいページ数
    INT 67h

    戻り値：AH = 00h ・・・ 正常実行
                            DX = ハンドル
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能
            AH = 85h ・・・ 空きハンドルがない
            AH = 87h ・・・ BXで指定したページ数は総ページ数を超えている
            AH = 88h ・・・ BXで指定したページ数は使用可能なページ数を超えている
            AH = 89h ・・・ BX=0であった

    一度にオープンできるファイルの上限が決まっているように、EMSも一度に使える プログラムの数が決まっています。EMSを使用している人数をハンドルという番号で 管理しており、そのハンドル数が足りなくなるとエラー(AH=85h)になります。また、 当然ですが、ページ数が足りなくてもエラーになります。

    割り当てたメモリーは解放しなくてはなりません。

デアロケート

    AH = 45h
    DX = ハンドル
    INT 67h

    戻り値：AH = 00h ・・・ 正常実行
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能
            AH = 83h ・・・ DX(ハンドル)が無効
            AH = 85h ・・・ 空きハンドルがない
            AH = 86h ・・・ ページマップのセーブを行い、リストアしていない


EMS

9-8. マップ、アンマップ

    EMSを割り当てたら、物理ページにマップします。すなわちノートのページをめくることです。

マップ、アンマップ

    AH = 44h
    AL = 物理ページ番号(0～3)
    BX = 論理ページ番号(アンマップする場合はBX=FFFFhにする)
    DX = EMSハンドル(AH=43hで割り当てたハンドル)
    INT 67h

    戻り値：AH = 00h ・・・ 正常実行
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能
            AH = 83h ・・・ DX(ハンドル)が無効
            AH = 8Ah ・・・ BXが無効
            AH = 8Bh ・・・ ALが無効

    例えば、5ページ分のEMSメモリーをアロケートした場合、下位から順に0,1,2,3,4と 論理ページ番号がふられます。最初のページ(0000h～3FFFh番地)にアクセスしたい場合は、 BX=0をマップし、そのページへのアクセスが終わり次のページ(4000h～7FFFh番地)に アクセスしたい場合はBX=1をマップします。


EMS

9-9. ページマップのセーブ、リストア

    これ以降は、ちょっと特殊な使い方をする場合です。普通のアプリケーションで EMSを使う場合はこれまでのファンクションだけで十分です。

    ページマップのセーブとは、主に常駐プログラムなどで使用するファンクションです。 つまり、EMSを使用している最中に、常駐プログラムが呼ばれ、かつその常駐プログラム がEMSを使用する場合、常駐プログラムが呼ばれる前のソフトが使っていたEMSページに しおりを挟んでおき、常駐プログラムが終了する際に、しおりを挟んだページ開いて おかなくてはいけません。

    しおりを挟むことを「ページのセーブ」、しおりをはさんだページを開くことを 「ページのリストア」と呼びます。

ページマップのセーブ

    AH = 47h
    DX = EMSハンドル(AH=43hで割り当てたハンドル)
    INT 67h

    戻り値：AH = 00h ・・・ 正常実行
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能
            AH = 83h ・・・ DX(ハンドル)が無効
            AH = 8Ch ・・・ セーブする領域がいっぱい
            AH = 8Dh ・・・ 指定されたページはすでにセーブされている

ページマップのリストア

    AH = 48h
    DX = EMSハンドル(AH=43hで割り当てたハンドル)
    INT 67h

    戻り値：AH = 00h ・・・ 正常実行
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能
            AH = 83h ・・・ DX(ハンドル)が無効
            AH = 8Eh ・・・ 指定されたハンドルはセーブされていない

    EMSを使用しているソフトが実行中に、EMSを使う常駐プログラムが起動したとします。 この常駐プログラムは、EMSを使う前に、ページマップをセーブします。このときに与える ハンドルは、常駐プログラムが使用しているEMSのハンドルです。

    そして、常駐プログラムはマップし、EMSを使用し、アンマップした後に、 常駐プログラム用のハンドルを用いてページマップをリストアします。これで 常駐プログラムはEMSに対する責任を果たしたことになり、呼び出し前のプログラムに 戻ることができます。


EMS

9-10. ハンドル数の取得

    使用できる総ハンドル数と、使用中のハンドル数を取得するファンクションを紹介します。

総ハンドル数の取得

    AX = 5402h
    INT 67h

    戻り値：AH = 00h ・・・ 正常実行
                            BX = 総ハンドル数
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能

使用中のハンドル数の取得

    AH = 4Bh
    INT 67h

    戻り値：AH = 00h ・・・ 正常実行
                            BX = 使用中のハンドル数
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能


EMS

9-11. ハンドルページの取得

    指定したハンドルに割り当てられているページ数の取得

ハンドルページの取得

    AH = 4Ch
    DX = EMSハンドル
    INT 67h

    戻り値：AH = 00h ・・・ 正常実行
                            BX = 指定ハンドルに割り当てられているページ数
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能
            AH = 83h ・・・ DXが無効


EMS

9-12. 再アロケート

    一度アロケートしたページ数を増減します。

再アロケート

    AH = 51h
    BX = 新しいページ数
    DX = EMSハンドル
    INT 67h

    戻り値：AH = 00h ・・・ 正常実行
                            BX = 新しいページ数
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能
            AH = 83h ・・・ DXが無効
            AH = 87h ・・・ BXで指定したページ数は総ページ数を超えている
            AH = 88h ・・・ BXで指定したページ数は使用可能なページ数を超えている

    BX=0にするとすべてデアロケートしますが、ハンドルだけを保持する場合に使用します。

    BXに現在のページと同じページを指定すると、何も動作しません。

    ページを増やす場合、ページ番号の最後に新しいページが付け加えられます。したがって、 今までの内容は壊れません。

    ページを減らす場合、ページ番号の最後から削除されます。残ったメモリー内容は 不変です。


EMS

9-13. ハンドル名

    今までハンドルと呼んできたものは、ハンドル番号という単なる数字でした。しかも それはEMMによって勝手に割り当てられるものです。したがって、データだけをメモリーに残して おき、プログラム自体は終了するが、次に起動したときにそのデータを使用するような プログラムは、EMSハンドル番号をどこかに記録しておかないと、EMSを引き続き使用することが できません。それではEMSの価値が下がってしまいます。

    ファイルの場合、ファイル名とファイルハンドルという２つの管理情報を使用することで この問題を解決しています。EMSもハンドルとは別に、ハンドル名という8バイトの情報を 一緒に用いることで、この問題をクリアすることができます。

    すなわち、プログラムが最初起動した時、EMSをアロケートし、ハンドルを取得します。 そのときにハンドル名を登録し終了します。次に起動されたときは、ハンドル名を頼りに ハンドル番号を取得することで、EMSにアクセスが可能になります。作業が終わったら 終了します。以降はこれを繰り返すことで、いつ起動されてもEMSを使用することができます。

ハンドル名の登録

    AX = 5301h
    DX = EMSハンドル
    DS:SI = ハンドル名の格納アドレス
    INT 67h

    戻り値：AH = 00h ・・・ 正常実行
                            BX = 新しいページ数
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能
            AH = 83h ・・・ DXが無効
            AH = A1h ・・・ 同じハンドル名がすでに登録されている

ハンドル名の取得

    AX = 5300h
    DX = EMSハンドル
    ES:DI = ハンドル名を格納するアドレス
    INT 67h

    戻り値：AH = 00h ・・・ 正常実行
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能
            AH = 83h ・・・ DXが無効

ハンドルの検索

    AX = 5401h
    DS:SI = ハンドル名の格納アドレス
    INT 67h

    戻り値：AH = 00h ・・・ 正常実行
                            DX = ハンドル名に相当するハンドル
            AH = 80h ・・・ EMM(ドライバー)が動作不能
            AH = 81h ・・・ EMS(ハードウェアー)が動作不能
            AH = A0h ・・・ 指定されたハンドル名は登録されていない
            AH = A1h ・・・ ハンドル名がない(EMM内部的な問題)


EMS

9-14. その他

    今まで紹介してきたファンクションはすべて基本的な部分です。EMS関連のファンクションは 全部で58個存在します(システム予約を含めて)。

AH or AX | 機　　　能
40h | ステータスの取得
41h | ページフレームアドレスの取得
42h | 未アロケートページ数の取得
43h | ページのアロケート
44h | ハンドルページのマップ/アンマップ
45h | ページのデアロケート
46h | EMSバージョンの取得
47h | ページマップのセーブ
48h | ページマップのリストア
49h、4Ah | システム予約
4Bh | ハンドル数の取得
4Ch | ハンドルページの取得
4Dh | 全ハンドルページの取得
4E00h | ページマップの取得
4E01h | ページマップの設定
4E02h | ページマップの取得と設定
4E03h | ページマップセーブ配列のサイズ取得
4F00h | ページマップの一部をセーブ
4F01h | ページマップの一部をリストア
4F02h | ページマップの一部をセーブする配列のサイズ取得
5000h 複数ハンドルページのマップ・アンマップ
(論理ページ / 物理ページ方式)
5000h 複数ハンドルページのマップ・アンマップ
(論理ページ / セグメントアドレス方式)
51h | ページの際アロケート
5200h | ハンドルアトリビュートの取得
5201h | ハンドルアトリビュートの設定
5202h | ハンドルアトリビュートのケイパピリティの取得
5300h | ハンドル名の取得
5301h | ハンドル名の設定
5400h | ハンドルディレクトリの取得
5401h | 指定ハンドルの検索
5402h | ハンドルの総数の取得
55h | ページマップの変更とジャンプ
56h | ページマップの変更とコール
5602h | ページマップスタックサイズの取得
5700h | メモリー領域の移動
5701h | メモリー領域の交換
5800h | マップ可能な物理アドレス配列の取得
5801h | マップ可能な物理アドレス配列エントリの取得
*5900h | ハードウェア構成配列の取得
*5901h | 未アロケートのローページ数の取得
5A00h | 標準サイズのページのアロケートと固有のEMMハンドルの割り当て
5A01h | ローページのアロケートと固有のEMMハンドルの割り当て
*5B00h | 代替マップレジスタセットの取得
*5B01h | 代替マップレジスタセットの設定
*5B02h | 代替マップセーブ配列のサイズ取得
*5B03h | 代替マップレジスタセットのアロケート
*5B04h | 代替マップレジスタセットの解放
*5B05h | DMAレジスタセットのアロケート
*5B06h | 代替マップレジスタ上の使用許可
*5B07h | 代替マップレジスタ上の使用不許可
*5B08h | DMAレジスタセットの解放
5Ch | ウォームブートのための拡張メモリーの準備
*5D00h | OS/Eファンクションセットの使用許可
*5D01h | OS/Eファンクションセットの使用不許可
*5D02h | アクセスキーのリターン
7000h | ページフレーム用バンクのステータス取得
7001h | ページフレーム用バンク状態の設定

    なお、ファンクション番号の頭に「*」がついている機能は、OSからの呼び出しにのみ 対応。

    ここでは基本的なものしか紹介しなかった。もし要望があれば、増やします。


XMS

10-1.XMSとは

    XMSは[eXtended Memory Specification]の略で、EMSと同様、MS-DOSで1Bytes以上の メモリーを使用するために作られた規格です。MS-DOSではEMSと同様、Ver 5.0から正式に サポートされました。

    実はXMSは次にあげる３つのメモリー管理法の寄せ集めです。その３つとは、 1) UMB、2) HMA、 3) EMBです。UMBについてはこのページの最初の方でも説明したので そちらも参照してください。

    これらの詳しい説明は後述するとして、簡単にどんなものかを紹介します。

UMB
    UMBとは[Upper Memory Block]の略で、通常BIOSなどのROM等が置かれている、A0000h～ FFFFFhまでの領域で使用していない部分をRAMとして使用し、コンベンショナルメモリー (00000h～9FFFFh)と同じように使用しようと言う規格です。

HMA
    HMAとは[High Memory Area]の略で、ハイメモリーとも略されます。これは1MBytes 以上の空間であるにもかかわらず、リアルモード(MS-DOS)で直接参照できる特別な 領域で、FFFFh:0010h～FFFFh:FFFFhの約64KBytes弱の領域です。

EMB
    EMBとは、[eXtended Memory Block]の略で、XMSドライバーを使用することで、 MS-DOSから参照できるメモリーです。HMAの上位に位置します。

    これら3つのうち、UMBは[INT　21h]を呼び出して使用します。HMA, UMBは XMSドライバーのファンクションを呼び出して使用します。まずHMA, EMB共通して 使用するファンクションの使い方を説明し、続いて、HMA, EMBの使用法を説明します。

    ちなみに仮想EMSは、EMBを使用して、あたかもEMSが存在するように見せかける、一種の シミュレーターの働きをするものです。


XMS

10-2. XMS使用法

    XMSを使用するためには、XMSドライバーが込む込まれているかを調べる必要があります。 これには、「INT　2Fh」というMS-DOSのシステムコールを使用します。

XMSドライバーの有無を調べる

    AX = 4300h
    INT 2Fh

    戻り値：AL = 80h : XMSドライバーは組み込まれている
            AL = それ以外 : XMSドライバーは組み込まれていない
    「INT　2Fh」というMS-DOSシステムコールは、MS-DOS Ver3.1で追加され、 MS-DOS Ver 5.0未満では非公開でした。このシステムコールは、マイクロソフトが 供給しているデバイスドライバー、常駐プログラムの組み込み状況を調べるものでした。

デバイスドライバー、常駐プログラムの組み込み情報の取得

    AH = プログラム番号(下記参照)
    AL = 00h
    INT 2Fh

    戻り値：AL = 00h : 組み込まれていない
                 01h : 同時に組み込めないものが組み込まれている
                 FFh : 組み込まれている
    プログラム番号
        01h : PRINT
        06h : ASSIGN
        10h : SHARE
        11h : REDIR
        12h : MSDOS.SYS
        B7h : APPEND
        B8h : XPORT

    さて、本題に戻ります。XMSドライバーのファンクションを呼び出すためには、今までのように 「INT　??h」を呼び出すのではなく、XMSドライバーを直接ファーコールします。そのための コール先を調べなければなりません。そのためには下記のようにします。

XMSドライバーのコール先の取得

    AX = 2F10h
    INT 2Fh

    戻り値：ES:BX = コール先

    これらをまとめると、XMSファンクションを呼び出すための準備は下記のようになります。
EMB_CHECK:			;embのﾁｪｯｸ
    mov	ax,4300h
    int	2fh
    cmp	al,80h
    jz	@@
    stc
    ret
@@:
    mov	ax,4310h
    int	2fh
    mov	[XMS_CALL+0],bx
    mov	[XMS_CALL+2],es
    clc
    ret
XMS_CALL	dw	0,0

;このサブルーチンは、XMSドライバが組み込まれていなければ、CF=1を返します。
;組み込まれていれば、[XMS_CALL]にコール先を格納します。
;使用するためには、下記のようにします。
;	mov	ah,0
;	call	far [XMS_CALL]


XMS

10-3.XMSバージョンの取得

XMSバージョンの取得

    AH = 0
    CALL  FAR [XMS_CALL]

    戻り値：AX = XMSバージョン
            BX = ドライバ内部リビジョン番号
            DX = 0000h : HMAが存在しない
                 0001h : HMAが存在する

    例えば、Version 2.35の場合、AX=0235hになります。ドライバー内部リビジョン番号は 普通のプログラムでは使用しません。


XMS

10-4.HMAとは

    アセンブラ入門で一番最初に学んだセグメントの概念を 思い出してください。16bitレジスタで20bitのアドレスを表現するために、セグメントと、オフセット というものを用いました。これはオフセットアドレスとセグメントアドレスをずらして足すという ものでした。

    そこでちょっと考えてください。1MBytesで一番上位、すなわちFFFFFhを表すためには、 [F000h:FFFFh]と表します。また、[FFFFh:000Fh]とも表せます。では、[FFFFh:0010h]という アドレスは、どこを指すのでしょうか？

    これはCPUによります。8086, 80186(, V30)では、これは[00000h=0000h:0000h]を指します。 つまり、１周して、最初に戻ってしまうわけです。(これをメモリーラップあるいはラップアラウンド と言います。) ところが、80286以上のCPUでは、 [10000h=FFFFh:0010h]を指します。すなわち1MBytes以上のメモリー空間に、MS-DOSから 直接アクセスできることを表します。

    したがって、MS-DOSから直接アクセスできる最大のメモリーは、[FFFFh:FFFFh]、すなわち [10FFEFh]番地までです。したがって、FFEFh=65519バイトだけ余計に使えるのです。そして この65519バイトの領域をHMAといいます。

    しかし、このHMAは80286以上であれば、どんな場合でもアクセスできるわけではありません。 80286以上のCPUに存在するアドレスバス32本のうち、21番目のアドレスバスが有効になっていないと 使用できません。この21番目のアドレスバスのことを「A20ライン」と呼びます。80286以上の CPUでもA20ラインが無効だと、8086などと同様にラップアラウンドして、メモリーの下位に 戻ってしまいます。逆にA20ラインが有効だと、HMAにアクセスできます。このA20ラインの 有効化、無効化はXMSドライバーのファンクションを使用します。

    コンベンショナルメモリーの管理は、セグメント単位で行っていました。しかし HMAはセグメント単位で管理ができません。なぜならばセグメントアドレスはFFFFhと 決まってしまっているからです。また64KBytes弱という、それほど大きくないメモリーを 、何人かで共有できるようにMS-DOSを改良すると、その分MS-DOS自体が使用するメモリーが 大きくなってしまい、本末転倒になってしまうため、「HMAは基本的に１人しか使用できない」 という仕様にしました。

    「基本的に」と言うのは、「１人」と限定するとあまりにも不親切なため、簡単な チェックでHMAの管理をしているからです。しかし、HMAを何人かで使用するのは、 不具合が生じる原因になるようです。また、デバイスドライバーや常駐プログラムは 何人かで使用できますが、アプリケーションはHMAが誰も使っていないと使用できません。

    ちなみにHMAを一番効率よく使用しているのは、MS-DOS自身をHMAに常駐させるときの ようです。


XMS

10-5.HMAの割り当て・解放

    HMAを使用するためには、XMSドライバの組み込みチェック、 XMSドライバーのコール先の取得を行い、XMSのバージョンを 取得して、その戻り値からHMAが存在するかどうかを調べなければなりません。

    そして、HMAを使用することを下記のファンクションにより宣言します。

HMAの要求

    AH = 01h

    [自分が常駐プログラムもしくはデバイスドライバーの場合]
    DX = 必要な容量(単位:Kバイト)

    [自分が普通のプログラムの場合]
    DX = FFFFh

    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : 割り当てられなかった
                 0001h : 割り当てられた

            AX = 0000hのとき
            BL = 80h : ファンクションのインプリメンテーションが行われていない
                 90h : HMAが存在しない
                 91h : HMAはすでに使用されている
                 92h : DXが"/HMAMIN="よりも小さい
    XMSドライバーを組み込む際に、CONFIG.SYSの中で、「これ以上小さい要求には応えるな」 というオプションがあります。[/HMAMIN=]というオプションです。その設定よりも小さい 要求の場合には[AX=0000h, BL=92h]というエラーが返されます。

    HMAの解放は下記の通りです。

HMAの解放

    AH = 02h
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : 解放できなかった
                 0001h : 解放した
            BL = 80h : ファンクションのインプリメンテーションが行われていない
                 90h : HMAが存在しない
                 93h : HMAは割り当てられていない


XMS

10-6. A20ラインの設定

A20ラインのグローバルな有効化

    AH = 03h
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : 有効化できなかった
                 0001h : 有効化された

            AX = 0000hのとき
            BL = 80h : ファンクションのインプリメンテーションが行われていない
                 82h : A20のエラー

A20ラインのグローバルな無効化

    AH = 04h
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : 無効化できなかった
                 0001h : 無効化された

            AX = 0000hのとき
            BL = 80h : ファンクションのインプリメンテーションが行われていない
                 82h : A20のエラー
                 94h : A20が有効化されていない

A20ラインのローカルな有効化

    AH = 05h
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : 有効化できなかった
                 0001h : 有効化された

            AX = 0000hのとき
            BL = 80h : ファンクションのインプリメンテーションが行われていない
                 82h : A20のエラー

A20ラインのローカルな無効化

    AH = 06h
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : 無効化できなかった
                 0001h : 無効化された

            AX = 0000hのとき
            BL = 80h : ファンクションのインプリメンテーションが行われていない
                 82h : A20のエラー
                 94h : A20が有効化されていない
    HMAにアクセスしたい場合は、「グローバルな有効化・無効化」を行います。EMBに アクセスしたい場合は、「ローカルな有効化・無効化」を行います。

    どちらもA20ラインを有効化したら、使用後には無効化しなければなりません。 当然ながら、「グローバルな有効化」を行ったら「グローバルな無効化」、 「ローカルな有効化」を行ったら「ローカルな無効化」を行います。

    また、A20ラインの状態を確認する場合は、下記のファンクションを使用します。

A20ラインの状態を取得

    AH = 07h
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : A20ラインは無効
                 0001h : A20ラインは有効
            BL = 00h : ファンクションは正常に実行された
                 80h : ファンクションのインプリメンテーションが行われていない


XMS

10-7. EMBとは

    EMBは拡張メモリーとも呼ばれ、アドレスで言うと10FFF0h以降のメモリーです。 仮想EMSは、このEMBを使用して、EMSに見せかけるためのドライバーです。

    では、EMSとEMBは何が違うのでしょうか？　EMSはページフレームという、のぞき穴を 通して1MBytes以上のメモリーにアクセスしています。このページフレームは、16KBytesを １ページとして、最大４ページまで割り当てることができます。一般にページフレームは ROMなどの領域で、使用していない1MBytes以内の場所に設けられます。

    しかしROMの状況によっては、ページフレームを用意できない場合などがあることと、 16KBytesを１ページという、固定ののぞき穴であるということが、不便な点でありました。

    EMBは、ページフレームの大きさは決まっていません。1Byteからでも良いのです。 また、ページフレームの場所もプログラムで動的に設定できます。ただし640KByte以内の コンベンショナルメモリーにとります。

    したがって、ページフレームを通して、EMBをのぞき見ると言うよりも、XMSドライバー の助けを借りて、コンベンショナルメモリーにEMBをコピーし、アクセスが終わったら コンベンショナルメモリーをEMBに書き戻すという感じです。

    EMSよりも不便な点があります。EMSはハンドル番号の他に、ハンドル名を用いることが できるので、メモリーにデータだけを残して終了し、次回起動したときにはそのデータを 使うという処理をするプログラムも、プログラムを終了してハンドル番号がわからなくなっても ハンドル名からハンドル番号を取得し、またEMSにアクセスできました。しかし、EMBは ハンドル番号だけで管理していますので、ハンドル番号を忘れると、ハンドル番号0から すべて探さなくてはいけなくなります。これにはかなりの時間がかかります。このような 使い方をしたい場合は、コンベンショナルメモリーをアロケートし、ハンドル番号を 保管しておかなくてはなりません。

    では、EMBの使い方を解説します。


XMS

10-8. EMBの空き容量取得

    EMBを使用するためには、XMSドライバの組み込みチェック、 XMSドライバーのコール先の取得を行わなければなりません。そしてその後、 使いたい容量EMBが残っているかを調べる必要があります。

EMBの空き容量取得

    AH = 08h
    CALL  FAR [XMS_CALL]

    戻り値：AX = 最も大きい連続した空きメモリーの容量
            DX = 空き容量の合計

            AX = 0 かつ DX = 0 のとき
            BL = 80h : ファンクションのインプリメンテーションが行われていない
            BL = A0h : EMBの空き容量はない


XMS

10-9. EMBの割り当て・解放

    空き容量が残っていたら、いよいよ割り当てをします。他のメモリーと同様、 使用後は解放します。

EMBの割り当て

    AH = 09h
    DX = 割り当てたいメモリーの大きさ(単位:KBytes)
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : 割り当てられなかった
                 0001h : 割り当てられた
            DX = ハンドル(割り当てに失敗した場合は0000h)

            AX = 0000hの時
            BL = 80h : ファンクションのインプリメンテーションが行われていない
            BL = A0h : EMBの空き容量はない
            BL = A1h : ハンドル数が足りなかった

EMBの解放

    AH = 0Ah
    DX = ハンドル
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : 解放できなかった
                 0001h : 解放した
            DX = ハンドル(割り当てに失敗した場合は0000h)

            AX = 0000hの時
            BL = 80h : ファンクションのインプリメンテーションが行われていない
            BL = A2h : ハンドルが無効
            BL = ABh : EMBがロックされている


XMS

10-10. EMBのコピー

    EMBを使うためには、コンベンショナルメモリーをEMBにコピーしなければなりません。 また、二度目に使う場合(EMBを書き換える場合)はEMBをコンベンショナルメモリーにコピーして、 コンベンショナルメモリーで書き換えた後にEMBにコピーしなければなりません。そのための ファンクションは以下の通りです。

EMBののコピー

    AH = 0Bh
    DS:SI = コピーパラメータを格納しているアドレス
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : コピーに失敗した
                 0001h : 正常にコピーされた

            AX = 0000hの時
            BL = 80h : ファンクションのインプリメンテーションが行われていない
            BL = 82h : A20ラインエラー
            BL = A3h : SourceHandle(コピーもとハンドル)が無効
            BL = A4h : SourceOffset(コピーもとアドレス)が無効
            BL = A5h : DestHandle(コピー先ハンドル)が無効
            BL = A6h : DestOffset(コピー先アドレス)が無効
            BL = A7h : Length(コピーするバイト数)が無効
            BL = A8h : 無効な重複部分がある
            BL = A9h : パリティーエラー

Length	      DD  ?  ; コピーするバイト数　　←DS:SIでここを指す
SourceHandle  DW  ?  ; コピー元ハンドル
SourceOffset  DD  ?  ; コピー元アドレス
DestHandle    DW  ?  ; コピー先ハンドル
DestOffset    DD  ?  ; コピー先アドレス
    コンベンショナルメモリーのハンドルは0000hです。EMBのハンドルは、EMB割り当ての ファンクションの戻り値です。コンベンショナルメモリーのアドレスは、通常のセグメントと オフセットアドレスです。EMBの場合は、普通の16進数で指定します。Lengthはコピーしたい サイズをバイト単位で指定します。

    強制ではありませんが、アドレスは偶数番地から偶数番地へコピーし、かつLengthが 偶数の場合は、処理速度が速くなります。80386以上の場合は、4の倍数のアドレスから、 4の倍数のアドレスへ、かつLengthが4の倍数の場合は、さらに処理速度が速くなります。 (MS-DOS付属のHIMEM.SYSの場合。サードパーティー製のドライバは不明です。)

    このファンクションは、EMB→EMB、EMB→コンベンショナルメモリー、コンベンショナルメモリー →EMB、コンベンショナルメモリー→コンベンショナルメモリーのいずれにも対応しています。

    転送元と転送先が重複する場合は注意が必要です。下位アドレスから上位アドレスに コピーされていきます。これは指定できませんので、上位アドレスから、下位アドレスに むけてコピーはできません。

    コンベンショナルメモリー間のコピーを例に取ると、8000h:0000hから7000h:0000hへ 1000hBytesコピーする場合には重複していないので問題ありません。しかし、7000h:0000hから 7000h:0010hへ20hBytesコピーする場合(下図上段)、7000h:0000hから7000h:000Fhの内容が 7000h:0010hへコピーされ、7000h:0010h～7000h:001Fhが上書きされます(下図中段赤字)。 その後、7000h:0010h～7000h:001Fhがコピーされるので、正常にコピーされません(下図下段赤字)。

    下記に、EMBコピーのサンプルサブルーチンを紹介します。
;+--------------------------+
;|EMB移動                   |
;+--------------------------+
;|in  bx:ax =size           |
;|in  cx    =source handle  |
;|in  ds:si =source address |
;|in  dx    =dest   handle  |
;|in  es:di =dest   address |
;+--------------------------+
COPY_EMB:
    mov	bx,offset EMB_MOVE_STRUCT
    mov	[ds:bx+08],ds			;source segment
    mov	[ds:bx+00],ax			;length(下位)
    mov	[ds:bx+02],bx			;length(上位)
    mov	[ds:bx+04],cx			;source handle
    mov	[ds:bx+06],si			;source offset
    mov	[ds:bx+10],dx			;dest   handle
    mov	[ds:bx+12],di			;dest   offset
    mov	[ds:bx+14],es			;dest   segment
    mov	si,bx
    mov	ah,0bh
    callf	XMS_CALL
    or	ax,ax
    jz	short @@
    clc
    ret
@@:	stc
    ret
EMB_MOVE_STRUCT	dw	8 dup(?)

        ;例）8000h:0100hから7000h:0200hへ1000hBytesコピーする場合
    mov	ax,8000h
    mov	ds,ax
    mov	si,0100h        	; DS:SI = SourceOffset
    mov	ax,7000h
    mov	es,ax
    mov	di,0200h        	; ES:DI = DestOffset

    xor	bx,bx
    mov	ax,1000h        	; Size = 1000h Bytes
    xor	cx,cx           	; SourceHandle
    xor	dx,dx           	; DestHandle
    call	COPY_EMB        	; Copy

        ;例) MS-DOSが使用できる1Mバイト分のメモリーをEMBにコピーする
    xor	ax,ax
    mov	bx,10h          	; Size = 1MBytes
    xor	cx,cx           	; SourceHandle = 0
    mov	ds,ax
    xor	si,si           	; SourceOffset = 0000h:0000h
    mov	dx,[cs:EMB_HANLDE]	; dx = Handle of EMB
    mov	es,ax
    xor	di,di           	; DestOffset = 00000000h
    call	COPY_EMB


XMS

10-11. EMBのロック・アンロック

    EMBをロックすると、不用意に書き換えられることがありません。

EMBのロック

    AH = 0Ch
    DX = ロックするEMBのハンドル
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : ロックされなかった
                 0001h : ロックした
            DX:BX = ロックしたEMBのリニアアドレス

            AX = 0000hの時
            BL = 80h : ファンクションのインプリメンテーションが行われていない
            BL = A2h : ハンドルが無効
            BL = ACh : ロックしているEMBが多すぎる
            BL = ADh : その他の理由でロックできない

EMBのアンロック

    AH = 0Dh
    DX = アンロックするEMBのハンドル
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : アンロックされなかった
                 0001h : アンロックした

            AX = 0000hの時
            BL = 80h : ファンクションのインプリメンテーションが行われていない
            BL = A2h : ハンドルが無効
            BL = AAh : DXで指示されたハンドルはロックされていない
    ロックできるEMBの数には限りがあるため、ロックする必要のない場合は ロックしない。またロックする必要が無くなった場合も、早めにアンロックする。

    リニアアドレスとは、指定したハンドルが実際に割り当てられている、絶対アドレスです。 普通にEMBを使用する場合には、ハンドルと、オフセットアドレスで管理するので、絶対アドレスは 必要ありません。例えば、EMBが128MBytesあるが、実際にはどのアドレスに格納されているのか 知りたいときには、このファンクションを使用すれば得られます。


XMS

10-12. EMBハンドル情報取得

    指定したハンドルの詳しい情報が得られます。

EMBハンドル情報取得

    AH = 0Eh
    DX = EMBのハンドル
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : 情報が見つからない
                 0001h : 情報が得られた

            AX = 0000hの時
            BL = 80h : ファンクションのインプリメンテーションが行われていない
            BL = A2h : ハンドルが無効

            AX = 0001hの時
            BH = EMBのロックカウント
            BL = EMBの空きハンドル数
            DX = 指定EMBのサイズ(単位:KBytes)


XMS

10-13. EMBの再割り当て

    一度割り当てたEMBのサイズの変更をしたい場合には下記のファンクションを利用します。

EMBの再割り当て

    AH = 0Fh
    BX = 新しいサイズ(単位:KBytes)
    DX = EMBのハンドル
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : 割り当てられなかった
                 0001h : 割り当てられた

            AX = 0000hの時
            BL = 80h : ファンクションのインプリメンテーションが行われていない
            BL = A1h : 使用可能なハンドルが足りない
            BL = A2h : ハンドルが無効
            BL = ABh : 指定したEMBはロックされている
    EMBの再割り当てには、一時的にハンドルが必要になります。したがって、使用可能な ハンドル数が足りない場合には、エラーになります。また、再割り当てしたいEMBが ロックされているときもエラーになります。

    容量を増やす場合には、今まで使用していた領域は破壊されず、そのまま使用できます。 容量を減らす場合は、元のEMBの上位にあるデータは捨てられます。


XMS

10-14. UMBの割り当て・解放

    UMBについては、「INT　21h」を使用する方法を最初の方でも 説明しているので、そちらも参照してください。

    UMBはUMBをリンクしていれば「INT　21h」を使用したシステムコールでも 使用可能ですが、ここではXMSファンクションを利用した使用法を説明します。

UMBの割り当て

    AH = 10h
    DX = 割り当てたいサイズ(16バイト単位)
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : 割り当てられなかった
                 0001h : 割り当てられた

            AX = 0000hの時
            DX = 実際に使用可能なUMBの最大サイズ(16バイト単位)
            BL = 80h : ファンクションのインプリメンテーションが行われていない
            BL = B0h : 指定したサイズのUMBがない
            BL = B1h : 使用可能なUMBがない

            AX = 0001hの時
            BX = 割り当てられたセグメントアドレス
            DX = 割り当てられたUMBのサイズ(16バイト単位)

UMBの解放

    AH = 11h
    DX = 解放したいUMBのセグメントアドレス
    CALL  FAR [XMS_CALL]

    戻り値：AX = 0000h : 解放できなかった
                 0001h : 解放した

            AX = 0000hの時
            BL = 80h : ファンクションのインプリメンテーションが行われていない
            BL = B2h : DXが無効
    このファンクションを使用して割り当てたUMBは、このファンクションで解放してください。 「INT　21h」のシステムコールと混在使用しないでください。

    使用可能な最大UMBを調べたい場合は、サイズFFFFhを割り当ててみようとします。 当然割り当てられませんが、そのときの戻り値(DX)から、情報を得ることができます。


アセンブラ ～MS-DOSの世界～

    アセンブラ ～MS-DOSの世界～へようこそ。

    今、アセンブラのコンパイラって結構手に入らないみたいですね。そこでフリーでダウンロードできる サイトを紹介します。

  • NOWSMART ASSEMBLER
  • 多摩ソフトウェア LASM Ver2.32 体験版
    また、私がよく読むアセンブラの本を紹介します。今手に入るかどうかはわかりませんが。。。
出版社 | タイトル | ISBN
CQ出版社 | IFセレクション MS-DOSメモリ管理ソフト技法 | 4-7898-3484-0
啓学出版 | 8086マシン語秘伝の書 | 4-7665-1079-8
マイクロソフト MS-DOS 5.0プログラマーズリファレンスマニュアル
インテル インテル・アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル、上巻：基本アーキテクチャ (日本語)
インテル インテル・アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル、中巻：基本アーキテクチャ (日本語)
インテル インテル・アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル、下巻：基本アーキテクチャ (日本語)
ボーランド Turbo Assembler 3.0 Quick Reference Guide
本の検索・ご購入は「 コンピュータの本・専門店」で。

←目次から選んでください

・ トップページに戻る


XMS

エスケープシーケンス

11-1.エスケープシーケンスとは

    カーソルを指定位置に移動したい場合や、テキストの色を変えたいなど、テキスト表示に関連する 操作を行いたい場合、通常はV-RAM(Video RAM)を直接操作します。しかしこの方法は面倒な操作ですし、 機種が異なれば異なったアドレスを操作しなければならないなど、機種依存性も生じてしまいます。 MS-DOSではこれらの操作を簡便に行えるように拡張しています。それがエスケープシーケンスです。

    MS-DOSは、マイクロソフトが作ったもの以外に、マシンを作ったメーカーが、そのマシンに特化した MS-DOSを作成しています。例えば、NECがPC-9801(PC-9821)用のMS-DOSを作成したりしています。 このときにエスケープシーケンスも各メーカーが一部機能追加をしている場合があります。このページ ではオリジナルのMS-DOSでサポートしているもののみを紹介します。


XMS

11-2.エスケープシーケンス表

    エスケープシーケンスは、1Bhに続く文字を標準出力に出力することで行います。 この1Bhはアスキーコードでエスケープを表しますので、エスケープシーケンスと呼びます。

    では、実際にどのようなことができるのかを紹介します。

エスケープシーケンス
コード | 機　能
ESC[pl;pcH | カーソルをpl行pc列に移動する
ESC[pl;pcf | カーソルをpl行pc列に移動する
ESC=lc | カーソルをl行c列に移動する。ただしlとcは20hが加えられた値となる
ESC[pnA | カーソルをpn行上の同一列に移動する
ESC[pnB | カーソルをpn行下の同一列に移動する
ESC[pnC | カーソルをpn文字分右に移動する
ESC[pnD | カーソルをpn文字分左に移動する
ESC[0J | カーソル位置から最終行の右端まで削除する
ESC[1J | 先頭行の左端からカーソル位置まで削除する
ESC[2J | 画面全体を消去し、カーソルを左上に移動する
ESC[* | 画面全体を消去し、カーソルを左上に移動する
ESC[0K | カーソル位置から同一行の右端まで削除する
ESC[1K | 同一行の左端からカーソル位置まで削除する
ESC[2K | カーソルのある行全て削除する
ESC[pnM | カーソル行から下へpn行削除する
ESC[pnL | カーソル行から上へpn行削除する
ESCD カーソルをカラム位置はそのままに１行下に移動する。
(カーソルが最下行にある場合は１行スクロールする)
ESCE カーソルを１行下の一番左に移動する。
(カーソルが最下行にある場合は１行スクロールする)
ESCM カーソルをカラム位置はそのままに１行上に移動する。
(カーソルが最上行にある場合は機種依存)
ESC[s | カーソル位置と表示文字の属性をセーブする。(機種依存)
ESC[u 上でセーブした内容をロードする。
セーブされていない場合はデフォルトの属性に戻し、一番左上にカーソルを移動する。(機種依存)
ESC)0 | 漢字モード(日本語モード)にする。(機種依存)
ESC)3 | 英語モードにする。(機種依存)
ESC[>1l | ファンクションキーの内容を画面に表示しない
ESC[>1h | ファンクションキーの内容を画面に表示する
ESC[>3n | 画面を31行モードにする(PC-9801シリーズのハイレゾモードのみ)
ESC[>3h | 画面を20行モードにする(PC-9801シリーズのみ?)
ESC[>3l | 画面を25行モードにする(PC-9801シリーズのみ?)
ESC[>5l | カーソルを表示させる
ESC[>5h | カーソルを非表示にする
ESC[ps;...;psm 文字属性を変更する
;で区切ることで、複数組み合わせることができる。
下表の表示例ではpsを１つだけ用いた表示例です。
psの値 | 内　容 | 表示例
0 | 既定値 | abc
1 | ハイライト(モノクロモードのみ)
2 | 縦線も表示
4 | アンダーラインつき | abc
5 | 点滅表示 | abc
7 | 反転して表示 | abc
8(または16) | 不可視 | abc
30 | 文字色を黒にする | abc
31(または17) | 文字色を赤にする | abc
32(または18) | 文字色を緑にする | abc
33(または19) | 文字色を黄色にする | abc
34(または20) | 文字色を青にする | abc
35(または21) | 文字色を紫にする | abc
36(または22) | 文字色を水色にする | abc
37(または23) | 文字色を白にする | abc
40 | 文字を黒反転にする | abc
41 | 文字を赤反転にする | abc
42 | 文字を緑反転にする | abc
43 | 文字を黄色反転にする | abc
44 | 文字を青反転にする | abc
45 | 文字を紫反転にする | abc
46 | 文字を水色反転にする | abc
47 | 文字を白反転にする | abc

例
CODE	SEGMENT
    ASSUME	DS:CODE,CS:CODE,ES:CODE,SS:CODE
    .186
    ORG	100h

START:
    mov	ah,9		;テスト表示
    mov	dx,offset _COLOR_UNDER
    int	21h

    mov	ax,0A05h
    call	_LOCATE

    mov	ah,9
    mov	dx,offset _TEST_DATA
    int	21h
EXIT:
    mov	ax,4c00h
    int	21h		;終了

;input ah = x
;input al = y
_LOCATE:
    push	ax		;レジスタ保管
    push	si;
    push	dx;

    push    ax
    mov     si, offset _LOCATE_DATA

    mov	ah, 0		;y座標の計算
    mov	dl,10h
    div	dl
    add	[cs:si+2], al
    add	[cs:si+3], ah

    pop	ax		;x座標の計算
    mov	al,ah
    mov	ah, 0
    div	dl
    add	[cs:si+5], al
    add	[cs:si+6], ah

    mov	ah,9		;表示
    mov	dx, si
    int	21h

    pop	dx
    pop	si
    pop	ax
    ret

_LOCATE_DATA	db	1Bh, '[', 30h, 30h, ';', 30h, 30h, 'H$'
_COLOR_UNDER	db	1Bh, "[4;36mabc", 1Bh, "[0m$";
_TEST_DATA	db	"123$"

    ENDS
    END	START
; 一部のコンパイラでは、
;CODE	ENDS
;	END	START
;と記述しなければいけないようです

abc

         123


アセンブラ ～MS-DOSの世界～

トップ->アセンブラ　～MS-DOSの世界～

あなたは

人目のアセンブラの受講生です。

アセンブラ入門

0. はじめに

1. コマンドラインからの引数
      1. コマンドラインからの引数
      2. 自分の名前は？

2. メモリー管理
      1. 環境変数
      2. 練習問題
      3. メモリーの割り当て
      4. メモリーの解放
      5. UMB
      6. メモリー管理
      7. 常駐プログラム(TSR)

3. 標準入出力
      1. １文字入力
      2. キーボードバッファー
      3. １文字出力
      4. 文字列出力
      5. 文字列入力
      6. 練習問題
      7. RS-232C
      8. プリンタ

4. ファイル操作
      1. ファイルオープン
      2. ファイルクローズ
      3. ファイルリード
      4. ファイルライト
      5. 練習問題1
      6. ファイルポインタの移動
      7. 練習問題2
      8. ファイル作成
      9. ファイル属性
      10. ファイル削除
      11. ファイル名変更
      12. ファイル属性の設定・取得
      13. ファイル日時の設定・取得
      14. ファイル検索
      15. 練習問題3
      16. 予約されているファイルハンドル
      17. ファイルハンドルの二重化(リダイレクト)

5. ディレクトリ操作
      1. ディレクトリ操作
      2. カレントディレクトリ変更
      3. カレントディレクトリ取得
      4. ディレクトリ作成
      5. ディレクトリ削除
      6. 練習問題

6. ディスク操作
      1. ディスクのリセット
      2. カレントディスクの取得
      3. カレントディスクの変更
      4. ドライブ情報の取得
      5. 空き容量取得
      6. 練習問題
      7. 交換可能ドライブの検査
      8. リトライ回数の設定
      9. ディスクの直接操作

7. プロセス管理
      1. プログラムの実行
      2. 練習問題
      3. プログラム終了
      4. リターンコード
      5. PSPアドレスの取得・設定
      6. DTAアドレスの取得・設定
      7. プログラムの実行2
      8. 練習問題2

8. その他のシステムコール
      1. 日付の取得・設定
      2. 時間の取得・設定
      3. 割り込みベクタの設定・取得
      4. ベリファイフラグ
      5. MS-DOSのバージョン取得
      6. 国別情報の取得・設定
      7. CTRL + Cの検査
      8. CTRL + Cの処理

9. EMS
      1. EMSとは
      2. EMSを使用するには
      3. EMSが使用可能か?
      4. EMSのバージョン取得
      5. ページフレームのアドレス取得
      6. 未使用ページ数の取得
      7. アロケート、デアロケート
      8. マップ、アンマップ
      9. ページマップのセーブ、リストア
      10. ハンドル数の取得
      11. ハンドルページの取得
      12. 再アロケート
      13. ハンドル名
      14. その他

10. XMS
      1. XMSとは
      2. XMS使用法
      3. XMSバージョンの取得
      4. HMAとは
      5. HMAの割り当て・解放
      6. A20ラインの設定
      7. EMBとは
      8. EMBの空き容量取得
      9. EMBの割り当て・解放
      10. EMBのコピー
      11. EMBのロック・アンロック
      12. EMBハンドル情報取得
      13. EMBの再割り当て
      14. UMBの割り当て・解放

11. エスケープシーケンス
      1. エスケープシーケンスとは
      2. エスケープシーケンス表

12. 終わりに

・ トップページに戻る


終わりに

    実はここで紹介したシステムコールの他にも色々とシステムコールがあります。 例えば、マウスドライバーや、グラフィックドライバーなどです。ほとんどのアプリケーションは これらを必要としませんので、ここでは紹介しません。特にグラフィックドライバーが 必要なアプリケーションは今まで1つしか見たことがありません。というのは、 それらが必要になるアプリケーションは、アセンブラのみで作成するのではなく、 C言語など高級言語で作成し、必要な部分だけをアセンブラで書く場合が多いからです。 もし要望があれば作りたいと思います。

    これで、何も要望や質問等がなければ、8086系CPUのアセンブラは終了します。 どうもご愛読(?)ありがとうございました。お疲れさまでした。

    質問、感想、文句、ご要望、アドバイスなどがありましたら、お気軽に メールください。

← 前ページへ　:　 トップへ　:　 次ページへ →


はじめに

    C言語があるということはB言語ってあるの？　その通りです。マイナーですがB言語という コンピュータ言語があり(実際には3年くらいの寿命でした)、それが進化したものがC言語です。 B言語の前には、BCPL言語、その前はALGOL言語というものがありました。ALGOL言語から進化した ものに、Pascalがあります。

    まったく別系統としてFORTRANがあり、それが進化したものがBASIC(Visual BASICとは まったく別物)、COBOLとFORTRANの長所をあわせたものがPL/Iという言語です。

    現在ではさらにたくさんのコンピュータ言語がありますが、C言語は未だに 多くの人に利用されている一般的な言語です。

    C言語はUNIXというOSをつくるために1972年、B.W.Kernighan氏とD.M.Ritchie氏により 考案され、1987年ANSI(米国標準協会)により標準C言語の規格が決定しました。

    C言語はOSを作成するために作られた言語であるため、アセンブラに近い処理も 可能であるし、C言語をアセンブラにコンパイルする仕方もANSIによって決まっているので、 アセンブラとC言語を組み合わせたプログラムも比較的簡単に作ることができます。 このようにアセンブラに近い処理も可能というのが、C言語最大の特徴です。 もう一つの特徴として、「ポインタ」というのがあります。数多く存在する コンピュータ言語のうちポインタを採用しているのはごく少数です。ポインタを 知らないでC言語はマスターできません。

    C言語では「命令」という概念はなく、「関数」という概念を導入します。 C言語プログラムは関数の集まりと、関数をどのような順番で計算させるかという 文法から成り立っています。 これら関数は、自分で作ることも当然できますし、 標準で多くの関数が用意されています。基本的にはこの関数をどのような順番で 呼び出すかというアルゴリズムをプログラマーが考えるだけです。

← 前ページへ　:　 トップへ　:　 次ページへ →


基本的な決まり

第1章. 基本的な決まり

1-1. 書式

    アルゴリズム入門で、C言語のプログラムを いくつか紹介してあるので、だいたいお分かりかと思いますが、次のような構成になっています。

  • #include <?????>
  • 関数の宣言
  • 変数の宣言
  • 関数群(実際のプログラム)
    最初の「#include」は、標準で用意されている関数(以下標準関数と略す)を使用する場合の 宣言です。今は「おまじない」と覚えて置いてください。例えば、画面に文字を表示する 「printf」関数や、キーボードから文字を入力する「scanf」関数を使用したい場合は 「stdio.h」というファイルを必要とします。その場合に「#include <stdio.h>」という １行を最初に書かなければなりません。

    次の「関数の宣言」とは、自分が作った関数の書式を最初に宣言しなければなりません。 これはコンパイラがソースファイル(「*.c」という自分が作ったファイル)の一番最初から コンパイルしていくので、この宣言がないと、コンパイラがどのようにコンパイルするのか わからず、エラーになってしまいます。自分が作った関数が、使う部分よりも前にあるときは 必要ありませんが、一応宣言しておく方が間違えが少なくなり、良いと思います。

    なお、最初の「#include」は、標準関数の宣言の集まりだと思っていただいて結構です。

    「変数の宣言」は関数同様、変数を宣言するものです。アセンブラでは変数はプログラマーが それが何ビットの大きさの変数で、符号付きなのか、符号なしなのかなどの情報を知っているので、 その通りに書いていけば良かったのですが、これではプログラマーが変数の情報を忘れてしまうと 思うようにプログラムが動きません。C言語ではこのようなことがないように、あらかじめ 変数の取り扱い方(大きさ、符号付き・符号なしの区別、整数か小数かなど)をコンパイラに 教えておきます。これが変数の宣言です。変数の宣言はどこで宣言するかによって、 どこでその変数を使えるかが変わってきます。これについては追々説明します。

    関数群(実際のプログラム)はプログラムの核となる部分です。「main」という名前の 関数から実行されます。main関数から標準で用意されている関数(printfなど)を呼び出したり、 自分で作った関数を呼び出したりします。

#include <stdio.h>

// ↓関数の宣言
void init();
void insert_sort();
void disp();

// ↓変数の宣言
int	A[10];
int	n;

// ↓関数群(実際のプログラム)
int main(int argc, char* argv[]){
    init();
    insert_sort();
    disp();
    return 0;
}

void init(){
    A[0] =  84;
    A[1] = 121;
    A[2] =  43;
    A[3] =  93;
    A[4] = 140;
    A[5] =  83;
    A[6] =  14;
    A[7] =  93;
    A[8] = 181;
    A[9] =  58;
    n    =  10;
}
void insert_sort(){
    int i = 1;
    while(i < n){
        int j = i;
        while(j != 0){
            if( A[j-1] > A[j] )
                j = 0;
            else{
                int a = A[j-1];
                A[j-1] = A[j];
                A[j] = a;
                j--;
            }
        }
        i++;
    }
}
void disp(){
    int i = 0;
    for ( ; i < n ; i++ )
        printf("%d   ",A[i]);
    printf("\n");
}


基本的な決まり

1-2. 予約語

    自分で作る変数や関数などの名前は「識別子」と呼びます。識別子の名前は基本的には プログラマーが自由に命名できます。ただし次のような制約があります。

  • 半角英数字および半角アンダーバー(_)のみ使用でき、最初の1文字は英字である
  • 大文字、小文字は区別される
  • 31文字以内であること(古いコンパイラでは最初の6文字)
  • 以下の予約語は使用できない
予約語
    auto	break	case	char	const	continue
    default	do	double	else	enum	extern
    float	for	goto	if	int	long
    register	return	short	signed	unsigned	void
    volatile	while
    また、慣用的に使われる決まりもあります。C言語の上位であるC++言語や、 C++言語と(文法的に)似ているJava言語では当てはまらないのがやっかいなのですが・・・。
  • 基本的に小文字を使う
  • 標準関数と同じ名前は使わない
  • 標準関数、予約語の大文字、小文字だけの違いのものは使わない


基本的な決まり

1-3. 構成

    C言語を文法的に見た場合の構成について説明します。

語句の種類
    C言語を構成する語句は「識別子」、「予約語」、「定数」、「文字列」、「演算子」、 「区切子」、「コメント」、「ブランク」があります。

    識別子はすでに説明したように、関数名、変数名のことです。変数には、 単純変数、配列、ポインタの３種類があり、さらにint型、char型などの型が 存在します。

    予約語は、プログラムの実行順序の制御や、変数の型など、決まっている語です。

    定数はその名の通り、定数です。文字列も定数(文字)の羅列で、定数の１種と考えられます

    演算子は足し算、引き算などの「+」や「-」のことです。

    区切子とは、文と文を区切ったりするものです。

    コメントとは、何をしている部分だとか、この関数は何をするものだとか、ここはまだ 未完成だよとか、プログラムとは関係ないが、プログラマーが自分あるいは、一緒に作っている 仲間のプログラマーに対してのメッセージです。コメントの書き方は２種類あります。１つは 「//」の以降改行まで、２つ目は「/*」と「*/」の間です。 後者は入れ子ができません。前者はコンパイラによってはサポートされていません。

    ブランクとは、プログラムを見やすくするための改行や、タブ、スペースを言います。

式
    これらの語句を組み合わせて式を構成します。

    式には一次式と演算式(演算子を用いて一次式を組み合わせたもの)に分けることができます (数学で言う一次式とは異なります)。

    関数も式になります。何かを処理するための引数を必要とし、その結果を返します。 その結果を戻り値と言います。

文
    式１つもしくは複数の式から文を構成します。文の後には必ず１文の終わりを示す記号 「;」がつきます。１行の中に文が２つ以上あっても構いません。エディターで見た場合に １行の中に複数の文があっても、コンパイラは１文を１文として認識するからです。

    したがって、次の左のプログラムと右のプログラムは同じものです。
void main(){
    int x = 10;
    int y = 20;
    int z = x + y;
}
void main(){
    int x = 10; int y = 10; int z = x + y;
}

    文には「式の文」(上で言う「int x = 10;」など)、空文(「;」のみからなる文)、 制御文(「break;」、「return;」など)があります。

ブロック
    文が0文、１文、あるいは複数の文からブロックが構成されます。文をブロックを表す 「{」と「}」で囲みます。ブロックは「１文と見なす単位」のことです。

    条件分岐(「if」、「else」)や反復(「for」や「while」など)などは、基本的に１文しか 実行できません。しかし１文だけしか繰り返せないのでは不便です。そこで このブロックで１文と見なすことで、複数の文を反復することができるのです。

    また、ブロックは入れ子にできるので、複数のブロックを１つのブロックとすることも できます。


基本的な決まり

1-4. データの型と宣言

    前にも紹介したように、変数を使う場合には宣言をしなくてはなりません。 何を宣言するかというと、どんなデータの型かと、名前、初期値です。初期値は 省略することができます。

    どこで宣言をするかというと、関数の外か、ブロックの最初です。 関数の外で宣言した場合、グローバル変数といい、そのファイルの中のすべての場所で 使用することができます。ただしこれはあまり良い宣言方法ではありません。 なぜならば、複数のプログラマーで大きいプログラムを作っている場合、 不正に書き換えられてしまう可能性があり、バグの発生につながる可能性があるからです。 特別な理由がある場合を除いて、後者のブロックの最初で宣言するべきです。

    次にデータの型について説明します。C言語では次の型が用意されています。

データの種類 | 型 | バイト長 | 数値の範囲
整数 | (signed) char | 1 | -128～+127
整数 | unsigned char | 1 | 0～+255
整数 | (signed) short (int) | 2 | -32768～+32767
整数 | unsigned short (int) | 2 | 0～+65535
整数 | (signed) long (int) | 4 | -2147483648～+2147483647
整数 | (signed) long (int) | 4 | 0～+4294967295
整数 (signed) int 2 or 4 (signed) short (int)もしくは
(signed) long(int)と同じ
整数 unsigned int 2 or 4 (signed) short (int)もしくは
(signed) long(int)と同じ
小数 | float | 4 | 仮数部232、指数部28
小数 | double | 8 | 仮数部252、指数部211
    括弧で書かれている部分は省略することができます。int、unsigned intは、古いコンパイラ では2バイト長、新しいコンパイラでは4バイト長で確保されます。したがって、絶対に 2バイトでなければならない場合は、short、unsigned short、4バイトでなければならない場合は long、unsigned longで宣言します。どちらでも構わない場合はint、unsigned intで宣言します。

    char型は1バイトで、文字1文字を格納するのにちょうど良い大きさです。しかし 数値を格納するにはちょっと小さな変数です。したがって主に１文字を格納するのに 使いますが、文字を格納する変数ではありません。小さな数字しか格納しないのであれば、 char型を使用しても構いません。

    なぜintが2バイトの場合と4バイトの場合があるかは、コンピュータの進化に関係しています。 16bitコンピュータで4バイト長の変数を操作するのには、処理に時間がかかります。 そこで数字のデフォルトをint型として、shortにしていました。しかしどうしてもshortでは 小さい場合にはlong型も使えるようにしました。コンピュータが32bitに進化すると、 long型を処理する場合にもshort型と同じ時間で処理できるようになったため、 数字型変数のデフォルトint型とlong型を同じにしたのです。このような経緯があるため、 数字のデフォルトであるint型がコンパイラによって異なるという問題が発生しました。

    さて、話を戻して、宣言の仕方を解説しましょう。

宣言は最初にお話ししたように、ブロックの最初、もしくは関数の外です。関数の外に 宣言するグローバル変数はもっと先でお話しします。当分の間はブロックの最初で次の ように宣言します。

    int a;		// 変数名「a」をint型で宣言
    char ch1, ch2;	// 変数名「ch1」と「ch2」をchar型で宣言
    unsigned long l;	// 変数名「l」をunsigned long型で宣言

    変数の宣言を行っただけだと、 その変数にはどんな値が入っているかわかりません。


基本的な決まり

1-5. 定数と変数

    宣言した変数に値を代入する方法を説明しますが、その前に定数について説明します。

整数定数
    整数定数には次のような種類があります。
・10進数		100	255	-50
・16進数		0x64	0xFF
・8進数  		0144	0377
・文字  		'd'
    このように8進数を表す場合には、最初に「0」を、16進数で表す場合には 「0x」をつけます。文字はASCII文字１バイトです。整数定数と言うよりは、 １文字の定数として扱うことがほとんどですが、整数として扱うこともできます。 つまり、C言語では(というかほとんどのプログラム言語では)文字という概念は なく、その文字のASCIIコードという数字で扱います。

小数定数
    小数の場合は普通の書き方ともう一つの方法があります。普通の書き方は、 「1.234」、「-0.364」のような書き方です。

    しかしこの書き方では100万や100万分の一などを書きたい場合、この書き方は 不便です。そこで「1e6」や「1E-6」のように、10の何乗かで表現できます。 「e」と「E」はどちらでも構いません。

    したがって、円周率を表すには、「3.1415」「0.31415E1」、「31415E-5」など と表すことができます。

代入
    宣言した変数に対して、定数を代入することができます。
    int a, b;		// 変数名「a」をint型で宣言
    char ch1, ch2;	// 変数名「ch1」と「ch2」をchar型で宣言
    unsigned long l;	// 変数名「l」をunsigned long型で宣言
    float f;  	// 変数名「f」をfloat型で宣言

    ch1 = 'A';
    ch2 = 0x4C;	// 16進数で4Chを代入
    l = 'C';  	// long型lに'C'のASCIIコード4Chを代入
    f = 3.1415;
    a = b = 100;	// aとbに100を代入
    一番最後の「a = b = 100;」は実は複雑な使い方です。詳細は 後述します。ここではaにもbにも100が代入されると覚えておいて ください。

初期化
    変数を宣言しただけだと、どんな値が入っているかわかりません。変数の宣言と 同時にある値で初期化することもできます。
    int a = 100;
    char ch = 'A';
    int b = a = 5;   	// aもbも5になる
    int n = m = 100;  	// このような使い方はダメ!!


基本的な決まり

1-6. 簡単な演算

    演算は式と式を演算子でつなぎます。C言語の演算子は非常に多くの種類があります。 詳しくは5章を参照してください。ここでは簡単なものを 紹介します。

代入演算子
    コピー演算子とも言います。変数に定数を代入したり、変数に変数に代入する場合に 用います。他の多くのコンピュータ言語と同じように「=」を用います。

    「char」型の変数の値を「int」型に代入する場合のように、大きい変数に小さい変数 に代入する場合は問題ありません。しかしこれとは逆に「int」型の変数の値を「char」型 の変数に代入する場合、値が丸め込まれます。すなわちデータの一部が欠落すると言うこと です。大抵は16進数で見た場合の下位の値がそのまま入ります。この場合、コンパイラによっては コンパイル時に警告が発せられます。これをさけるためにはキャストといい、 型変換を行う必要があります。これについては後述します。

    int n1, n2, n3;
    char c1, c2, c3;

    c1 = 100;
    n1 = c1;	// 代入先が型の方が大きいので問題なし

    n2 = 100;
    c2 = n2;	// n2もc2も100が入る

    n3 = 128;
    c3 = n3;	// n3は128、c3は-128が入る
                 // n3もc3も16進数でみると、両方0x80で同じ値

算術演算子
    算術演算子とは、「+」「-」「*」「/」「%」です。それぞれ、加算、減算、乗算、 除算、余りを求めるです。整数同士のわり算の場合、結果も整数になります。この場合、 小数点以下は切り捨てになります。また小数での計算で余りを求めることはできません。

    これらの計算の順序は、数学の場合と同じです。

    また、負数を含む「余り(%)」を計算する場合は、コンパイラによって異なります。 自分のコンパイラではどんな結果になるか確かめてみてください。

比較演算子
    等値か「==」、非等値か「=!」、大小関係「>」「>=」「<=」「<」を調べます。
演算子 使用例 　意　味
< a < b aはbより小さい
<= a <= b aはb以下
> a > b aはbより大きい
>= a >= b aはb以上
== a == b aはbと同じ
!= a != b aとbは異なる
    成り立つ場合は「真」すなわち1が、成り立たない場合は「偽」すなわち0となります。

比較の「==」と代入の「=」を間違わないように気をつけてください。初心者がよく間違える 部分です。

    int a = 10;
    int b = 20;
    int c;

    c = a >  b;	// c = 0
    c = a >= b;	// c = 0
    c = a <  b;	// c = 1
    c = a <= b;	// c = 1
    c = a == b;	// c = 0
    c = a != b;	// c = 1
論理演算子
    真と偽を論理演算で評価します。C言語では真とは0以外の数値、偽とは0を表します。
演算子 使用例 　意　味
&& a && b AND(かつ)
|| a || b OR(または)
! !b NOT(否定)
    int a = 10;
    int b = 20;
    int c;

    c = a && b;             	// c = 1
    c = !b;	                	// c = 0
    c = a == 20 && a < b ;	// c = 1
    これら論理演算子、比較演算子は、場合によって見にくくなる場合もありますので、 括弧をつけて自分のセンスで見やすくなるように心がけましょう。例えば、上の例は のようにします。
    int a = 10;
    int b = 20;
    int c;

    c = a && b;             	// c = 1
    c = !b;	                	// c = 0
    c = (a == 20) && (a < b) ;	// c = 1


入出力

第2章. 入出力

2-1. printf

    画面に文字を表示させるには標準ライブラリ関数を用います。すなわちすでに用意されている 関数を用いるので、比較的簡単に表示できます。数字だろうが、文字だろうが、16進数だろうが 簡単です。アセンブラ入門を読んでくれた方はお分かりだと思いますが、アセンブラで 数字を表示させるのは、非常に面倒な作業でした。

    以下の例のように、C言語では非常に簡単です。

#include <stdio.h>

void main(){
    printf("Hello C world.\n");
}

Hello C world.
    上の例のように表示したいダブルクォーテショーンで囲んだ文字列を表示します。 最後の「\n」はエスケープシーケンスと言い、「\」の後の文字でいろいろな 文字を表示します。後で詳しく説明しますが、「\n」は改行を示します。

    このように簡単に文字を表示することができますが、これではまだ不便で、 変数の値を表示したい場合もあります。その場合は、「%」記号を使います。

#include <stdio.h>

void main(){
    int	n  = 10;
    char	ch = 'A';
    printf("変数nの値は%dです。\n",n);
    printf("chの文字は%cです。\n",ch);
}

変数nの値は10です。
chの文字はAです。


入出力

表現指示文字

    前ページの例のような「%d」の「d」のような文字を 「表現指示文字」と言います。表現指示文字は以下のような物がありますが、 すべてのコンパイラで使用可能なわけではなく、サポートされていない物もあるようです。

表現指示文字 | 機　能
d | 整数を10進数で表示
x | 整数を16進数で表示
o | 整数を8進数で表示
u | 符号なし整数を10進数で表示
s | 文字列を表示
c | 変数を１文字として表示
e | 小数を指数形式で表示
f | 小数を実数形式で表示
g | 小数を指数形式、実数形式の短い方で表示

表現指示文字(その２)

    さらに桁をあわせたい場合などには、以下の表現指示文字を加えます。

符号 + データを右詰めで表示(符号付き)
- データを左詰めで表示(負数のみ符号付き)
省略 | データを右詰めで表示(負数のみ符号付き)
0 | 0 | 数値が入らない桁を「0」で埋める
省略 | 数値が入らない桁をスペースで埋める
桁数 | n | 符号を含めて「n」桁で表示する」
省略 | 必要な桁分表示する
小数部 n 小数「n」桁まで表示する
文字列の場合は表示する文字数
省略 | 必要な桁分表示する
    例を示します。
#include <stdio.h>

void main(){
    int	n  = 10;
    double	pi = 3.1415926;
    printf("変数nの値は%+010dです。\n",n);
    printf("円周率は%+010.5fです。\n",pi);
}

変数nの値は+000000010です。
円周率は+003.14159です。

エスケープシーケンス(制御文字)

    エスケープシーケンスは、改行やタブなど特殊な操作をしたい場合に用いられます。 以下のようなエスケープシーケンスがありますが、グレー の物は機種・OSによっては表示できない物もあります。

エスケープシーケンス | 機　能
\n | 改行(次の行の先頭に行く)
\t | 水平タブ
\\ 円記号「\」(機種・OSによってはバックスラッシュ)
\' シングルクォーテーション「'」
\" ダブルクォーテーション「"」
\0nn nnは0～7までの2桁の8進数
8進数でnnが表す文字
\xnn nnは0～Fまでの2桁の16進数
16進数でnnが表す文字
\b | １文字戻る
\r | その行の先頭に戻る
\f | 画面をクリアする
\a | ベルを鳴らす
\v | 垂直タブ


入出力

2-2. scanf

    scanfはキーボードから数値や文字、文字列を入力し、変数に代入します。 これもアセンブラで書こうとすると面倒な処理ですが、C言語では簡単に書けます。

    ただし、scanf関数はかなりのバグや、バグを生む危険性を持っています。 使用する場合は、プログラマーも、そのプログラムを使う人も注意しないと、 暴走します。しかし、scanf関数は簡単なので、C言語の初級用の解説書などには 頻繁に出てくる関数です。当ホームページでは、後々練習問題として、scanf関数に 代わる入力関数を作ってもらう予定です。

    scanf関数はprintf関数と逆の関数と思ってください。使い方も似ています。 以下に例を示します。

#include <stdio.h>

void main(){
    int	n;
    char	ch;

    printf("数字を入力してください --> ");
    scanf("%d", &n);
    printf("文字を入力してください --> ");
    scanf("%c", &ch);

    printf("\n\n");
    printf("入力された数字は %d です\n", n);
    printf("入力された文字は %c です\n", ch);
}

数字を入力してください --> 10 [return]
文字を入力してください --> a [return]

入力された数字は 10 です
入力された文字は a です
    上の例のように入力したい形式をダブルクォーテショーンで囲みます。 カンマ「,」に続いて、変数を記述します。ただし、文字列以外は 変数の前に「&」をつけます。 この「&」については、後述します。

    上述したバグの例を示します。

  • 数字を入力するときに、アルファベットや記号を入力する
  • １つめのscanfの時に「1 A」のようにスペースを入れる
  • 数字の変数「n」を「int」型ではなく「short」型で宣言する
    これらの場合、コンパイラなどにより症状はかわりますが、 おそらくプログラマーの期待する動作とは異なる動作をしたり、暴走したりする でしょう。(以降、「コンパイラやOSによって・・・」というのを「処理系の違いによって・・・」と 表現します。)

    printfではいろいろな(10進数とか16進数とか、小数とか)型を表示できました。 scanfでも以下のような変換指定文字が用意されており、いろいろな型を入力できます。

変換指定文字 | 機　能
d | 整数を10進数で入力
x | 整数を16進数で入力
o | 整数を8進数で入力
e | 小数を指数形式で入力
f | 小数を実数形式で入力
c | 変数を１文字として入力
s | 文字列を入力
p | ポインタ

    また、scanfでは複数の値を入力することもできます。以下に例を示します。

#include <stdio.h>

void main(){
    int	n1, n2;
    char	ch;

    printf("数字を２つ入力してください --> ");
    scanf("%d %d", &n1, &n2);

    printf("\n\n");
    printf("入力された数字は %d と%d です\n", n1, n2);
}

数字を２つ入力してください --> 10 100[return]

入力された数字は 10 と 100 です


入出力

2-3.練習問題1

問題
キーボードから16進数を１つ入力し、それを10進数で表示するプログラムを作れ。


入出力

2-3.練習問題1

問題
キーボードから16進数を１つ入力し、それを10進数で表示するプログラムを作れ。

解答
#include <stdio.h>

void main(){
    int n;

    scanf("%x", &n);
    printf("%d\n", n);
}


入出力

2-4. 1文字入出力

    printf、scanfは高機能な入出力関数でしたが、１文字の入出力を行いたい時に 使うには、「実行プログラムが大きくなる」「実行速度が遅くなる」などの短所も ありました。そこで、ここでは１文字に限った入出力関数を紹介します。 ただし、1文字の入出力はその用途によっていくつか種類がありますが、 ここでは次の物を紹介します。

int  getch()	１文字入力(エコーなし)
int  getche()	１文字入力(エコーあり)

int  putch(int c)	１文字出力
    例を示します。
#include <conio.h>

void main(){
    int	ch;
    ch = getch();	// 1文字入力

    printf("\n");	// 改行

    putch(ch);	// 1文字出力
}

a

a
    黄色の部分「a」を入力しますが、画面には「a」は表示されません。改行を挟んだ 次の「putch」で初めて表示されます。「getch()」を「getche()」にすると、入力した 文字も表示され、計２つの「a」が表示されることになります。

    ここで注意して欲しいことがあります。「getch()」の戻り値、「putch()」の引数 は「char」型ではなく「int」型ということです。「char」型にすると、コンパイラにも よりますが、警告(warning)が出る場合があります。

    ここで紹介しなかった１文字の入力関数は「CTRL+C」を受け付けると中止した旨の 「-1」を返す関数があります。この場合「char」型だと、「-1」が受け取ることができなく なるために、「int」型にしています。それらの関数と互換を保つために、ここで紹介した 関数も「int」型なのです。


入出力

2-5.エスケープシーケンス

    カーソルを指定位置に移動したい場合や、テキストの色を変えたいなど、テキスト表示に関連する 操作を行いたい場合、通常はV-RAM(Video RAM)を直接操作します。しかしこの方法は面倒な操作ですし、 機種が異なれば異なったアドレスを操作しなければならないなど、機種依存性も生じてしまいます。 MS-DOSではこれらの操作を簡便に行えるように拡張しています。それがエスケープシーケンスです。

    MS-DOSは、マイクロソフトが作ったもの以外に、マシンを作ったメーカーが、そのマシンに特化した MS-DOSを作成しています。例えば、NECがPC-9801(PC-9821)用のMS-DOSを作成したりしています。 このときにエスケープシーケンスも各メーカーが一部機能追加をしている場合があります。このページ ではオリジナルのMS-DOSでサポートしているもののみを紹介します。

  ※エスケープシーケンスはもともとMS-DOS系OSのものであるため、他のOSでは使用できません。


入出力

    エスケープシーケンスは、1Bhに続く文字を標準出力に出力することで行います。 この1Bhはアスキーコードでエスケープを表しますので、エスケープシーケンスと呼びます。

    では、実際にどのようなことができるのかを紹介します。

エスケープシーケンス
コード | 機　能
ESC[pl;pcH | カーソルをpl行pc列に移動する
ESC[pl;pcf | カーソルをpl行pc列に移動する
ESC=lc | カーソルをl行c列に移動する。ただしlとcは20hが加えられた値となる
ESC[pnA | カーソルをpn行上の同一列に移動する
ESC[pnB | カーソルをpn行下の同一列に移動する
ESC[pnC | カーソルをpn文字分右に移動する
ESC[pnD | カーソルをpn文字分左に移動する
ESC[0J | カーソル位置から最終行の右端まで削除する
ESC[1J | 先頭行の左端からカーソル位置まで削除する
ESC[2J | 画面全体を消去し、カーソルを左上に移動する
ESC[* | 画面全体を消去し、カーソルを左上に移動する
ESC[0K | カーソル位置から同一行の右端まで削除する
ESC[1K | 同一行の左端からカーソル位置まで削除する
ESC[2K | カーソルのある行全て削除する
ESC[pnM | カーソル行から下へpn行削除する
ESC[pnL | カーソル行から上へpn行削除する
ESCD カーソルをカラム位置はそのままに１行下に移動する。
(カーソルが最下行にある場合は１行スクロールする)
ESCE カーソルを１行下の一番左に移動する。
(カーソルが最下行にある場合は１行スクロールする)
ESCM カーソルをカラム位置はそのままに１行上に移動する。
(カーソルが最上行にある場合は機種依存)
ESC[s | カーソル位置と表示文字の属性をセーブする。(機種依存)
ESC[u 上でセーブした内容をロードする。
セーブされていない場合はデフォルトの属性に戻し、一番左上にカーソルを移動する。(機種依存)
ESC)0 | 漢字モード(日本語モード)にする。(機種依存)
ESC)3 | 英語モードにする。(機種依存)
ESC[>1l | ファンクションキーの内容を画面に表示しない
ESC[>1h | ファンクションキーの内容を画面に表示する
ESC[>3n | 画面を31行モードにする(PC-9801シリーズのハイレゾモードのみ)
ESC[>3h | 画面を20行モードにする(PC-9801シリーズのみ?)
ESC[>3l | 画面を25行モードにする(PC-9801シリーズのみ?)
ESC[>5l | カーソルを表示させる
ESC[>5h | カーソルを非表示にする
ESC[ps;...;psm 文字属性を変更する
;で区切ることで、複数組み合わせることができる。
下表の表示例ではpsを１つだけ用いた表示例です。
psの値 | 内　容 | 表示例
0 | 既定値 | abc
1 | ハイライト(モノクロモードのみ)
2 | 縦線も表示
4 | アンダーラインつき | abc
5 | 点滅表示 | abc
7 | 反転して表示 | abc
8(または16) | 不可視 | abc
30 | 文字色を黒にする | abc
31(または17) | 文字色を赤にする | abc
32(または18) | 文字色を緑にする | abc
33(または19) | 文字色を黄色にする | abc
34(または20) | 文字色を青にする | abc
35(または21) | 文字色を紫にする | abc
36(または22) | 文字色を水色にする | abc
37(または23) | 文字色を白にする | abc
40 | 文字を黒反転にする | abc
41 | 文字を赤反転にする | abc
42 | 文字を緑反転にする | abc
43 | 文字を黄色反転にする | abc
44 | 文字を青反転にする | abc
45 | 文字を紫反転にする | abc
46 | 文字を水色反転にする | abc
47 | 文字を白反転にする | abc

例
#include <stdio.h>

#define ESC             ((char)0x1b)
#define LOCATE(x, y)    printf("%c[%d;%dH", ESC, y, x )

void main(){
    printf("%c[4;36mabc%c[0m", ESC, ESC);

    LOCATE(10,5);
    printf("123\n");
}

abc

         123


制御文

第3章. 制御文

    制御文とは、プログラムの流れを制御するものです。今までの例でもお分かりのように、 C言語も他の多くのプログラム言語と同様、上から下にプログラムは実行されます。 このように上から下に実行される流れを「順次構造」といいます。しかし、 ある処理を繰り返したり、条件によって異なる処理をさせたい場合は、順次構造 以外の流れが必要になります。

    このようなプログラムの流れは以下のような物があります。

  • 順次構造
  • 分岐構造
    • 単一分岐構造
    • 多重分岐構造
    • ケース構造
  • 反復構造
    • 前判定型反復構造
    • 後判定型反復構造
    C++言語やJava言語ではこれに加えて「例外」という構造がありますが、 ここでは省略します。

    アルゴリズムのページも参照してください。

3-1. 順次構造

    上述のように、上から下にかけて実行されるという、もっとも単純な構造です。 フローチャートでいうと右図のようになります。


制御文

3-2.単一分岐

    分岐の条件が１つだけの場合を単一分岐という。フローチャートの書き方は以下のようになる。 また、C言語の書き方を青で示した。

変数nが0なら処理1を、
0以外なら処理2を実行する 		nがm以上なら処理1を、
未満なら処理2を実行する
    if(n==0){
        処理1;
    }
    else{
        処理2;
    }

    if(n>=m){
        処理1;
    }
    else{
        処理2;
    }

charの内容が'A'であれば処理1を実行し、
それ以外ならば何もしない
同上
こういう書き方もある

    if(ch=='A'){
        処理1;
    }
    else{
        処理2;
    }

    どの例もすべて、処理をし終わった後は合流していることが重要です。 これは多重分岐などの場合も含めて、必ず合流しなくてはなりません。

    次に例を示します。下の例では、キーボードから数字を入力し、それが10以上 20未満なら「10代」と表示します。

#include <stdio.h>

void main(){
    int n;
    scanf("%d", &n);

    if ( 10 <= n && n < 20 )
        printf("10代\n");
}
    上の例のように、「10以上」という条件と「20未満」という条件を 「&&」で区切ることで、 その両方を満たす場合に限り、次の文が実行されます。

    さらに、複数の条件のうちどれか一つでも満たされていれば実行させたい場合は、 「&&」の代わりに、「||」で区切ります。

    また、条件が満たされない場合の処理がなければ、「else」文を省略することも できます。


制御文

3-3.多重分岐

    多重分岐とは、単一分岐を組み合わせたもので、単一分岐の処理の中にまた条件分岐を 含むものである。

C言語での表記
if(条件1){
    if(条件2){
        処理3;
    }
    else{
        処理2;
    }
}
else{
    処理1;
}

    以下に例を示します。以下の例では、キーボードから数字を入力し、その数字が 負数なら「負数」、0なら「0」、整数なら「整数」と表示する。２例を示すが、 どちらも同じ結果になる。

#include <stdio.h>

void main(){
    int  n;
    scanf("%d",&n");

    if ( n < 0 ){
        printf("負数\n");
    }
    else{
        if ( n == 0 ){
            printf("0\n");
        }
        else{
            printf("正数\n");
        }
    }
}

#include <stdio.h>

void main(){
    int  n;
    scanf("%d",&n");

    if ( n < 0 )
        printf("負数\n");
    else if ( n == 0 )
        printf("0\n");
    else
        printf("正数\n");
}
    上の例のように、多重分岐での「else」は直前の「if」に対応します。


制御文

3-3.多重分岐(例題)

  例題：
変数a,b,cに適当な数が代入されている。この変数中で最小値を変数wに代入する。

  考え方：
aとbを比べる。そのうち、小さい方とcを比べる。その結果小さい方が最小値である。

  下のアンダーラインを埋めよ。
  • a≦bでかつa≦cの時　→　最小値は_______________
  • a≦bでかつa＞cの時　→　最小値は_______________
  • a＞bでかつb≦cの時　→　最小値は_______________
  • a＞bでかつb＞cの時　→　最小値は_______________


制御文

3-3.多重分岐(例題)

  例題：
変数a,b,cに適当な数が代入されている。この変数中で最小値を変数wに代入する。

  考え方：
aとbを比べる。そのうち、小さい方とcを比べる。その結果小さい方が最小値である。

  • a≦bでかつa≦cの時　→　最小値は　a
  • a≦bでかつa＞cの時　→　最小値は　c
  • a＞bでかつb≦cの時　→　最小値は　b
  • a＞bでかつb＞cの時　→　最小値は　c

これをアルゴリズムで表すと、
if(a<=b){
    if(a<=c)
        w=a;
    else
        w=c;
    }
else{
    if(b<=c)
        w=b;
    else
        w=c;
    }
}


制御文

3-3.多重分岐

    前のページでは、多重分岐を用いて最小値を求めた。しかしこのような方法では、 ３つの変数から最小値を求めるだけで、条件分岐を３つも用いており、変数が増えた場合 とんでもないことになってしまう。そこで「書き換え法(仮称)」という方法がある。

  例題：
変数a,b,cに適当な数が代入されている。この変数中で最小値を変数wに代入する。

  考え方：
これはとりあえず１つめの変数を仮の最小値とする。 次の変数を仮の最小値と比較して小さければ、その値を仮の最小値にする。 これを変数の数だけ繰り返す。

void main(){
    int a,b,c,w;
    a=100;
    b=200;
    c=300;

    w=a;

    if(b<w)
        w=b;

    if(c<w)
        w=c;

    printf("最小値は%dです\n",w);
}

  変数が多くなればこの方法でも、どんどんプログラムが長くなります。 もちろん実際のプログラムでは、どんどん長く書くわけではありません。 チェックする部分は同じような処理をさせているわけですので、後で述べる 反復構造を利用するのです。


制御文

3-4.ケース(多方向分岐)

    ３つ以上の異なった処理の中から、条件にあった処理を行うことを多方向分岐と 言い、次のような書き方をする。

例）　charaの内容が'A'だったらwに65を、'B'ならば66をそれ以外なら0を代入する。

switch(chara){
    case 'A' : w=65; break;
    case 'B' : w=66; break;
    default  : w=0 ; break;
}

    なお、C言語表記中の「break;」を忘れ、下のようなプログラムにすると、 左のフローチャートになる。しかし、このようなフローチャートは悪い見本であり、 正式なフローチャートではない。(正しく動かないという意味ではなく、 このようなアルゴリズムがきれいなアルゴリズムではないという意味。)

switch(chara){
    case 'A' : w=65;
    case 'B' : w=66;
    default  : w=0 ;
}

    「switch」文の括弧の中は、どんな型でも良いわけではなくて、 「int」型、「char」型、あるいは後述する「enum」型のみです。


制御文

例題

    西暦を入力し、その年の干支を表示させなさい。ただし西暦0年は申年とする。
#include <stdio.h>

void main(){
    int year;

    printf("西暦を入力してください : ");
    scanf("%4d", &year);		// 西暦入力

    if ( year < 0 )
        year = -year;		// 正に直す

    switch( year % 12 ){		// 12で割ったあまり
    case 0 :
        printf("%d年は申年です\n", year);
        break;
    case 1 :
        printf("%d年は酉年です\n", year);
        break;
    case 2 :
        printf("%d年は戌年です\n", year);
        break;
    case 3 :
        printf("%d年は亥年です\n", year);
        break;
    case 4 :
        printf("%d年は子年です\n", year);
        break;
    case 5 :
        printf("%d年は丑年です\n", year);
        break;
    case 6 :
        printf("%d年は寅年です\n", year);
        break;
    case 7 :
        printf("%d年は卯年です\n", year);
        break;
    case 8 :
        printf("%d年は辰年です\n", year);
        break;
    case 9 :
        printf("%d年は巳年です\n", year);
        break;
    case 10 :
        printf("%d年は午年です\n", year);
        break;
    case 11 :
        printf("%d年は未年です\n", year);
        break;
    }
}


制御文

3-5. 反復構造(while)

    ここで紹介するwhile文は、前判定型であり、繰り返し処理に入る前に 繰り返し条件をチェックする。したがって、条件によっては繰り返し処理を １度も実行しない場合もあり得る。

    1から100までの合計を求めてみる。

void main(){
    int n, t;

    n = 1;
    t = 0;

    while(n <= 100){
        t = t + n;
        n = n + 1;
    }
    printf("1から100までの合計は%dです\n",t);
}
    while文ではwhileの後の括弧の中に条件式を記述し、その条件式が真になる間 処理が繰り返される。もちろん、複数の条件式を「&&」「||」でつなげることもできる。


制御文

3-6. 反復構造(do)

    ここで紹介するdo～while文は、後判定型であり、繰り返し処理を行った後に 繰り返し条件をチェックする。したがって、どんな条件でも最低１回は繰り返し処理が 実行される。

    前ページ同様、1から100までの合計を求めてみる。

void main(){
    int n, t;

    n = 0;
    t = 0;

    do{
        t = t + n;
        n = n + 1;
    }while(n <= 100);

    printf("1から100までの合計は%dです\n",t);
}
    do～while文ではwhileの後の括弧の中に条件式を記述し、その条件式が真になる間 処理が繰り返される。もちろん、複数の条件式を「&&」「||」でつなげることもできる。


制御文

3-7. 反復構造(for)

    ここで紹介するfor文は、前判定型であり、繰り返し処理に入る前に 繰り返し条件をチェックする。したがって、条件によっては繰り返し処理を １度も実行しない場合もあり得る。

    for文は、while文に初期設定と、変化式を組み合わせたものです。これまでの 例と同様に1から100までの合計を求めてみる。while文を使用すると下記のようになる。

void main(){
    int n, t;

    n = 1;			// 初期設定
    t = 0;			// 初期設定

    while(n <= 100){
        t = t + n;
        n = n + 1;	// 変化式
    }
    printf("1から100までの合計は%dです\n",t);
}
    上の赤字の部分は、繰り返し処理に入るための 初期設定をしている部分で、緑字の部分は、 次回の繰り返し処理のための最初期化をする部分である。for文ではこれらを 一緒に組み込むことができる。実際には下記のようになる。
void main(){
    int n, t;

    for ( n = 1, t = 0 ; n <= 100 ; n = n + 1 ){
        t = t + n;
    }
    printf("1から100までの合計は%dです\n",t);
}
    すなわち、

    for ( 初期設定 ; 条件式 ; 変化式 )
        繰り返し処理
    となる。

    初期設定、変化式はカンマ「,」で区切って複数の式にすることもできるが、 あまり書かない。また、初期設定、変化式が必要ない場合は、必ずしも記述する必要はなく、 省略することもできる。しかしこの場合もセミコロン「;」は省略できない。その例を 下記に示す。

void main(){
    int n, t;

    n = 1;			// 初期設定
    t = 0;			// 初期設定

    for( ; n <= 100 ; n = n + 1){
        t = t + n;
    }
    printf("1から100までの合計は%dです\n",t);
}
    繰り返す処理が１文の時は、中括弧を省略し、下記のように書くこともできる。
void main(){
    int n, t;

    n = 1;
    t = 0;

    for( ; n <= 100 ; n = n + 1)
        t = t + n;

    printf("1から100までの合計は%dです\n",t);
}
    このように処理が１文の時に中括弧を省略できるのは「if」「else」文、「while」文も同様である。


制御文

3-8. 補助制御文

    反復構造では同じ処理を繰り返しましたが、場合によっては条件式が真でも 反復構造から抜け出したい場合が生じたりする場合もあります。このような 処理の流れはアルゴリズム的にはあまり良いことではないのですが、実際の プログラムでは、このように反復処理中にその処理から抜け出した方が、 プログラムがすっきり書ける場合も少なくありません。

    そのような時には補助制御文を使用します。補助制御文には以下のような物が 用意されています。

break
    「switch～case文」のところでも紹介しましたが、「while」「for」などの反復処理中に 使用すると、強制的に反復処理を抜けます。

    反復処理が入れ子になっている場合(多重反復処理)は、内側の反復構造を１つだけ 抜けるだけです。

continue
    反復構造中に使用すると、反復処理が終了したことになります。すなわち条件評価を 行い、条件が真ならばもう一度反復処理を実行します。なお、「for」文であれば、 変化式も実行した後に条件式を評価します。

goto
    ラベルへの無条件分岐です。C言語ではよっぽどのことがない限り使用しません。 この文が使われているプログラムを見たら、「よっぽどのことがあるのか、賢くない 人が作ったのかどちらかなんだ」と想ってください。

return
    その関数から抜けます。詳細は後述します。
    上で紹介した補助制御文のフローチャートを示します。
    以下に例を示します。この例では、数字を２つ入力しa,bに代入します。 そしてaからbまでの偶数の和を求めますが、bが100以上の場合はaから100までの偶数の 合計を求めます。なおaの方が大きい場合のチェックはしていません。
#include <stdio.h>

void main(){
    int a, b, t;

    scanf("%d %d", &a, &b);

    for ( t = 0 ; a <= b ; a++ ){
        if ( a > 100 )	// 100以上になったら終了
            break;
        if ( a % 2 )	// 偶数じゃなかったら、変化式を実行
            continue;
        t = t + a;
    }
    printf("%d\n", t);
}
    上の例の「continue」文の前の「if」文の条件ですが、 本題からはずれますが、ここで解説します。

    条件式は「真」か「偽」で判断します。C言語では「0」と「偽」は等価です。 また「0以外」と「真」は等価です。したがって、「a % 2」すなわち「a÷2」の 余りが真(0以外)ならば「continue」を実行します。

    したがって「if ( a % 2 )」は「aが奇数なら」という意味になります。


制御文

トップ-> C言語入門：制御文-> 3-9. 練習問題

3-9. 練習問題

問題
    キーボードから月(1～12)を入力し、その月の日数を表示するプログラムを作れ。

← 前ページへ　:　 解答へ　:　 次ページへ →


制御文

3-9. 練習問題

問題
    キーボードから月(1～12)を入力し、その月の日数を表示するプログラムを作れ。

解答
#include <stdio.h>

void main(){
    int month;
    printf("月(1～12)を入力してください : ");
    scanf("%d", &month);

    switch(month){
    case 1:
    case 3:
    case 5:
    case 7:
    case 8:
    case 10:
    case 12:
        printf("\n%d月は31日です\n", month);
        break;
    case 2:
        printf("\n2月は28日か29日です\n");
        break;
    case 4:
    case 6:
    case 9:
    case 11:
        printf("\n%d月は30日です\n", month);
        break;
    default:
        printf("\n1から12を入力してください\n");
    }
}


制御文

トップ-> C言語入門：制御文-> 3-10. 練習問題

3-10. 練習問題

問題
    数当てゲーム。ある数を設定して、それを当てるゲームを作る。正解よりも小さい 値を入力したら「小さい」と、大きい値を入力したら「大きい」と表示する。 入力する範囲は0から9999に限定し、それ以外が入力されたらもう一度入力させる。 10回行っても正解しない場合は終了する。

← 前ページへ　:　 解答へ　:　 次章へ →


制御文

問題
    数当てゲーム。ある数を設定して、それを当てるゲームを作る。正解よりも小さい 値を入力したら「小さい」と、大きい値を入力したら「大きい」と表示する。 入力する範囲は0から9999に限定し、それ以外が入力されたらもう一度入力させる。 10回行っても正解しない場合は終了する。
解答
#include <stdio.h>

void main(){
    int seikai = 1484;
    int count;
    int inp = -1;

    for ( count = 1 ; count <= 10 ; count++ ){
        inp = -1;
        while( inp < 0 || inp > 9999 )
            scanf("%d", &inp);

        if ( inp == seikai ){
            printf("%d回目で正解ですね\n", count);
            break;
        }
        if ( inp < seikai )
            printf("小さい\n");
        else
            printf("大きい\n");
    }
    if ( inp != seikai )
        printf("10回で正解しませんでした。ゲームオーバー\n");
}


配列

第4章. 配列

4-1.配列とは

    例えば100個のデータがあった場合、これを入れるための変数として、a1,a2,a3,・・・,a100と 変数を設定したのでは大変なことになってしまうことは明らかです。このデータの形式が 同じ場合、メモリー上に100個の連続した領域を確保し、何件ものデータを総括的に扱う ことができる方法を配列といいます。配列は、変数名と何番目のデータを指すかという 添え字で構成されています。

    例えば、１ヶ月のお店の来客数を管理するのに、raikyaku01,raikyaku02, raikyaku03,・・・raikya31なんてやっていたら大変なことになります。 そこでraikyakuという配列を日にちという添え字を用いて利用するわけです。具体的には raikyaku(1),raikyaku(2),・・・raikyaku(31)ってな具合です。

    配列を利用すると何が便利なのでしょうか？　それは添え字に変数が使えることです。 先ほどの１ヶ月の来客数を表示する場合、配列を使わないとraikyaku01, raikyaku02,raikyaku03,・・・raikya31と表示させたい変数を すべて列挙しなくてはなりません。配列を使うと、反復構造を使って、添え字の変数を 1ずつ増やしていくだけでいいのです。

    ところで、配列の添え字は0から始まるプログラム言語と、 1から始まるプログラム言語があります。。 C言語では0から始まります。これに対してCOBOLでは1から始まります。

    C言語では変数を使う前に、領域を確保しなければなりません。配列の場合も何番まで 使うかをあらかじめ宣言しなければなりません。宣言した 以上の値を添え字として用いた場合、他のデータ領域を破壊することになりかねません 。この場合どこにどんな影響があるかわかりません。最悪の場合、 プログラムが暴走する可能性もありまし、OS自体がおかしくなり、リセットする可能性も あります。この点はしっかりと注意してください。ただし、WindowsやUNIXでは OS自体がおかしくなることはまずありません。


配列

4-2.配列の宣言

    C言語での配列使用宣言は下記のようにします。
int raikyaku[10];
  宣言のみ
  [10]は配列の大きさ(0から9の10個分確保する)
char a[]={10,20,30,40,50}
  宣言　＋　数の代入
  この場合配列の大きさは省略できる
  省略した場合は代入を行った5つ分を確保する

    C言語ではraikyaku[10]のように添え字を大括弧で表現しますが、フローチャート ではraikyaku(10)のように普通の括弧で表現します。

    また、C言語では配列を使うと宣言しただけでは、変数と同様何が入っているかわかりません。 つまり使う前に配列の中身をきれいにしなくてはなりません。

    なお、C言語では変数を用いて配列の宣言を行うことはできません。すなわち 動的に配列のサイズを変化させることができないのです。C++言語ではできるように 改良されていますが・・・。

// 悪い例
void main(){
    int	i = 10;
    int	hairetsu[i];

     :
     :
     :
}


配列

    例を示します。
void main(){
    int a[10];
    a[0] = 10;
    a[1] = 20;
    a[2] = 30;
}
    というプログラムがあったとします。プログラム終了時には aという配列は下記のようになります。(?は何が入っているかわからない状態を示す。)
内容 10 20 30 ? ? ? ? ? ? ?
添え字 0 1 2 3 4 5 6 7 8 9
void main(){
    int a[10] = { 10, 20, 30 };
}
    というプログラムがあったとします。上の例では10個の配列を確保しますが、 初期化する文字は３文字しかありません。このような場合は残りの7個は0が 入るようになっています。
内容 | 10 | 20 | 30 | 0 | 0 | 0 | 0 | 0 | 0 | 0
添え字 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
    もし、「a[10] = 'C'」と書いた場合、配列の大きさを越えているので、 コンパイラによってはエラーもしくは警告を表示しますが、普通にコンパイルしてしまう コンパイラもあります。もし正常にコンパイルされてしまったとして、実行すると、 運がいいと何もなりませんが、運が悪いと暴走したりします。すなわち何が起こるか わからないと言うことになります。


配列

4-3.配列の使用法

    配列は添え字を用いることで、その要素の読み書きをします。添え字は変数であっても 構いません。

    「int型」の要素数10個の配列を作り、これに1から10の数字を代入する例を示します。

// 添え字に変数を使わない例
void main(){
    int a[10];

    a[0] = 1;
    a[1] = 2;
    a[2] = 3;
    a[3] = 4;
    a[4] = 5;
    a[5] = 6;
    a[6] = 7;
    a[7] = 8;
    a[8] = 9;
    a[9] = 10;
}

// 添え字に変数を使った例
void main(){
    int i;	// 添え字
    int a[10];

    for ( i = 0 ; i < 10 ; i++ )
        a[i] = i+1;
}
    このように、配列を使うことと、その添え字に変数を使うこと、反復処理を用いることで、 非常に便利になります。

    もし、上のプログラムを以下のように書き換えるとどうなるでしょう？

// 悪い例
void main(){
    int i;	// 添え字
    int a[10];

    for ( i = 0 ; i <= 10 ; i++ )
        a[i] = i+1;
}
    変更点は「for文」の条件式です。「=」を加えたことによって、 繰り返し処理の部分は11回実行されます。すなわち「a[10] = i + 1」が 実行されることになり、確保した配列の大きさを越えてしまっています。 つまり、実行時に何が起こるかわかりません。このようなミスは最初の頃は よくやるミスですので、気をつけてください。


配列

トップ-> C言語入門：配列-> 4-4. 練習問題

4-4. 練習問題

問題
    要素数10のchar型の配列を作り、「A」から「J」で初期化する。キーボードから 数字(0～9)を入力し、該当する文字を表示するプログラムを作れ。例えば、0を入力したら 「A」という文字を表示する。

← 前ページへ　:　 解答へ　:　 次ページへ →


配列

4-4. 練習問題

問題
    要素数10のchar型の配列を作り、「A」から「J」で初期化する。キーボードから 数字(0～9)を入力し、該当する文字を表示するプログラムを作れ。例えば、0を入力したら 「A」という文字を表示する。

解答
#include <stdio.h>

void main(){
    int  i;
    char a[]={'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'};

    printf("0から9までの数字を入力してください  : ");
    scanf("%d", &i);

    if ( 0 <=i && i <= 9 )
        printf("%c\n", a[i]);
}


配列

4-5.文字列

    C言語での文字列は配列の一種です。すなわち「char型」の配列に、 文字が連続で格納されているのが、文字列になります。文字列の最後には NULLという文字が入ります。NULLとは実際には文字コードが0の文字という意味で、 '\0'と表記します。

    このため、５文字からなる文字列を格納するためには、最低でも６文字分の 大きさを持つ配列が必要になります。

    文字列の宣言の例を示します。

   char str1[10] = "abcde";
   char str2[] = "abcde";
   char str3[] = {'a', 'b', 'c', 'd', 'e', '\0'};
    上記のような宣言をすると、以下のような配列ができます。
str1 | a | b | c | d | e | \0 | \0 | \0 | \0 | \0
str2 | a | b | c | d | e | \0
str3 | a | b | c | d | e | \0
    上記のようにダブルクォーテーションで囲うことによって、文字列での配列の初期化を 簡単に行うことができます。ただし、ダブルクォーテーションで文字列を指定することができるのは、 宣言の時のみで、一度宣言してある配列を文字列で初期化することはできません。 したがって下記の例はダメな例です。

   char str[10];
   str = "abcde";	// コンパイルエラー


配列

    次に「printf」や「scanf」関数で文字列を扱う方法を示します。

printf

    printf関数で文字列を扱うためには、表現指示文字として「%s」を用います。
#include <stdio.h>

void main(){
    char month[] = "January";
    printf("This month is %s.\n", month);
}

This month is January

scanf

    scanf関数で文字列を入力するためには、変換指示文字として「%s」を用います。
#include <stdio.h>

void main(){
    char name[20];
    printf("What's your name? : ");
    scanf("%19s", name);
    printf("Your name is %s.\n", name);
}

What's your name ? : Mike
Your name is Mike.

    数字や文字を入力する場合は、scanf関数で変数の前に「%」を つけました。しかし文字列の入力で、配列の名前には「%」をつけない ので気をつけてください。この理由については後述します。

    また、配列の大きさはあらかじめ指定しなければなりません。ここでは20文字分の 配列を確保しています。しかし文字列の最後にはNULL('\0')が付加されるため、 実際に入力できるのは、19文字になりますので、「%19s」と 指定しなければなりません。

puts, gets

    printfでは文字列と一緒に数字なども出力することができます。しかし 文字列だけの出力だけならば、「puts()」という関数も用意されています。

    また同様にscanfは文字列と一緒に数字などの入力も可能ですが、 文字列だけの入力だけならば、「gets()」という関数も用意されています。

    これらの例を示します。

#include <stdio.h>

void main(){
    char name[20];
    printf("What's your name?  : ");
    gets(name);
    puts(name);

//	puts(gets(name));　としても良い
}

What's your name?  : Mike
Mike
    上記の例の場合、20文字以上入力しないようにユーザー側が気をつけなくては ならない。


配列

4-6.２次元配列と多次元配列

    2次元配列とは、配列の配列です。イメージ的には表だと想ってください。
   int a[3][4];
   a[0][0] = 10;
   a[0][1] = 20;
   a[1][2] = 30;
   a[2][3] = 40;
    上記の例の結果は以下のような状態になります。
10 20 ? ?
? ? 30 ?
? ? ? 40
    下記のようにすると初期化できます。
   int  a[3][4] = { { 0, 1, 2, 3 }, { 4, 5, 6, 7 }, { 8, 9, 10, 11 }  };
   int  b[3][4] = { { 10 }, { 20 }, { 30, 31 } };
   char c[3][4] = { "abc", "de", "f" };
    これらの結果は以下のようになります。
配列a
0 | 1 | 2 | 3
4 | 5 | 6 | 7
8 | 9 | 10 | 11
配列b
10 | 0 | 0 | 0
20 | 0 | 0 | 0
30 | 31 | 0 | 0
配列c
'a' | 'b' | 'c' | '\0'
'd' | 'e' | '\0' | '\0'
'f' | '\0' | '\0' | '\0'
    １次元配列と同様に、配列の宣言時に配列の大きさを指定し、さらに 初期化を行う際に、足りない部分は「0」が入ります。
    また、初期化する場合は行の要素数を省略することができます。ただし、 列の要素数は省略できません。

    したがって上の例は下のように書いても同じになります。

   int  a[][4] = { { 0, 1, 2, 3 }, { 4, 5, 6, 7 }, { 8, 9, 10, 11 }  };
   int  b[][4] = { { 10 }, { 20 }, { 30, 31 } };
   char c[][4] = { "abc", "de", "f" };
    なお、２次元配列に文字列をscanfやgetsを用いて入力する場合は、
    char  a[3][4];
    scanf("%s", a[0]);
    scanf("%s", a[1]);
    scanf("%s", a[2]);
//  scanf("%s", a[3]);	// これはダメ
    のようにします。
    3次元配列以上の物を多次元配列と言います。原理的には何次元でも作ることができます。 例えば、３次元配列を作る場合には下記のようにします。
   int  a[10][10][10];
    しかし、例えばchar型で、1024×1024×1024の３次元配列を考えると、1024MBytesという 膨大なメモリーを消費することになります。


配列

トップ-> C言語入門：配列-> 4-7. 練習問題

4-7. 練習問題

問題
    4行20列の配列を作る。scanfを用いて名前を４人分入力してその配列に格納する。 そして確認のために、その４人分の名前をprintfで表示するプログラムを作れ。

← 前ページへ　:　 解答へ　:　 次章へ →


配列

4-7. 練習問題

問題
    4行20列の配列を作る。scanfを用いて名前を４人分入力してその配列に格納する。 そして確認のために、その４人分の名前をprintfで表示するプログラムを作れ。

解答
#include <stdio.h>

void main(){
    char meibo[4][20];
    int  i;

    // 名前入力
    for ( i = 0 ; i < 4 ; i = i + 1 ){
        printf("%d人目の名前を入力してください : ", i+1);
        scanf("%19s", meibo[i]);
    }

    // 改行
    puts("\n");

    //  確認のための表示
    for ( i = 0 ; i < 4 ; i = i + 1 )
        printf("%d人目は [%s] さんです\n", i+1, meibo[i]);
}

1人目の名前を入力してください : 宇多田ヒカリ
2人目の名前を入力してください : 倉木真衣
3人目の名前を入力してください : 阿部なつみ
4人目の名前を入力してください : 鈴樹あみ

1人目は [宇多田ヒカリ] さんです
2人目は [倉木真衣] さんです
3人目は [阿部なつみ] さんです
4人目は [鈴樹あみ] さんです


演算子

第5章. 演算子

5-1.演算子の種類

    １章では、簡単な演算について説明した。ここでは以下に示す 演算子について説明する。
  • 演算子
    • 算術演算子
      • 四則演算子
      • 符号変換
    • 比較演算子
      • 大小比較
      • 等価、非等価
    • 論理演算子
      • 論理否定
      • 論理積
      • 論理和
    • インクリメント演算子
    • デクリメント演算子
    • ビット演算子
      • 論理積
      • 論理和
      • 排他的論理和
      • 論理否定(1の補数)
    • シフト演算子
      • 左シフト
      • 右シフト
    • アドレス演算子
    • 間接演算子
    • 代入演算子
      • 代入
      • 四則演算
      • シフト演算
      • ビット演算
    • cast演算子
    • 順次演算子
    • 条件演算子
    • sizeof演算子

+, -, *, /, %
-

<, >, <=, >=
==, !=

!
&&
||
++
--

&
|
^
~

<<
>>
&
*

=
+=, -=, *=, /=, %=
<<=, >>=
&=, |=, ^=
(型)　例えば、(int), (char)など
,
? :
sizeof


演算子

5-2.インクリメント演算子、デクリメント演算子

    int i;
    for ( i = 0 ; i < 10 ; i = i + 1 )
        printf("%d\n", i);
    上記の例のように、今まで変数を１増やす場合「i = i + 1」と 書いてきた。しかし「変数を１増やす」と言う作業は頻繁に行われるため、 特別の演算子が用意されている。上の例は、その演算子を使用すると 下記のようになる

    int i;
    for ( i = 0 ; i < 10 ; i++ )
        printf("%d\n", i);
    この「++」がインクリメント演算子である。 逆に「変数の値を１減らす」演算子はデクリメント演算子と言い、 「--」である。

    この演算子は、「i++」のように変数の後に書くことも、「++i」 のように変数の前に書くこともできます。しかし「j = i++」のように 式の中に組み込むと、変数の前に書いた場合と、変数の後に書いた場合の動作が 異なります。変数の前に書いた場合は、式を評価する前に インクリメントします。逆に 変数の後に書いた場合は、式を評価した後にインクリメントします。

#include <stdio.h>

void main(){
    int a, b;

    a = 3;
    b = ++a;

    printf("a = %d,  b = %d\n", a, b);
}

#include <stdio.h>

void main(){
    int a, b;

    a = 3;
    b = a++;

    printf("a = %d,  b = %d\n", a, b);
}

a= 4, b = 4

a= 4, b = 3
    ここで注意があります。１つの式の中に 同じ変数に対するインクリメント演算子が複数あってはならない ということです。もちろんデクリメント演算子も同じ注意があります。

    例えば、「b = a++ + a++」というのはダメです。コンパイラによって 評価の仕方が異なるからです。「printf("%d %d\n", ++a, ++a);」という のもダメです。aが0のとき「1 2」と表示されるか「2 1」と 評価されるかわかりません。


演算子

5-3.複合代入演算子

    複合代入演算子とは、演算と代入を一つの演算子にまとめたものです。 インクリメント演算子やデクリメント演算子も 複合代入演算子の一種と言うことができます。複合代入演算子には以下のような 種類があります。
複合代入演算子 使用例 意　味 普通の演算子での記述
+= a+=b; aにbを加えた値をaに代入する a=a+b
-= a-=b; aからbを引いた値をaに代入する a=a-b
*= a*=b; aとbを乗じた値をaに代入する a=a*b
/= a/=b; a÷bをaに代入する a=a/b
%= a%=b; a÷bの余りをaに代入する a=a%b
>>= a>>=b; aをbビット右にシフトした値をaに代入する a=a>>b
<<= a<<=b; aをbビット左にシフトした値をaに代入する a=a<<b
&= a&=b; aとbの論理積をaに代入する a=a&b
|= a|=b; aとbの論理和をaに代入する a=a|b
^= a^=b; aとbの排他的論理和をaに代入する a=a^b


演算子

5-4.ビット演算子

    ビット演算子とは、各変数をビット単位で処理(計算)するための演算子で、 以下のような種類かがあります。
演算子 意　味 使用法
& ビット積(AND) a=b&c; bとcの各ビット毎の論理積をとり、その結果をaに代入する
| ビット和(OR) a=b|c; bとcの各ビット毎の論理和をとり、その結果をaに代入する
^ ビット排他的論理和(XOR) a=b^c; bとcの各ビット毎の排他的論理和をとり、その結果をaに代入する
~ ビット否定(NOT) a=~b; bの各ビットを反転しその結果をaに代入する
AND

b | c | 結果(a)
0 | 0 | 0
0 | 1 | 0
1 | 0 | 0
1 | 1 | 1
    AND演算はビットを0でマスクするのに使用します。マスクとは、あるビットを 0もしくは1に変えることを言います。例えば、奇数か偶数かを調べるためには、 最下位を残して0でマスクします。その値が1なら奇数、0なら偶数と言うことになります。
#include <stdio.h>

void main(){
    int i;

    scanf("%d", &i);
    if ( i & 1 )
        printf("奇数です\n");
    else
        printf("偶数です\n");
}

OR

b | c | 結果(a)
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 1
    OR演算はビットを1でマスクするのに使用します。

XOR

b | c | 結果(a)
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0
    XOR演算はビットを必要な部分だけ反転させるのに使用します。

NOT

b | 結果(a)
0 | 1
1 | 0
    NOT演算は１の補数を表します。１の補数とは全てのビットを反転します。


演算子

5-5.シフト演算子

    シフト演算とは論理演算と同じようにビットを操作する命令である。具体的には、 2進数の各ビットをずらすことである。右にずらすか左にずらすかの２種類の演算子が 用意されている
演算子 意　味 使用法
<< 左にシフト a=b<<c bを左にcビットずらす
>> 右にシフト a=b>>c bを右にcビットずらす
    一概にシフトするといっても、ずらすデータが符号付き整数(singed)か、符号なし整数 (unsigned)かによって、動作が異なる。なお、floatやdouble、後述するポインタ型には 使用できない。

    まず、符号なし整数のシフト命令について説明する。 これらは論理シフトと言い、ずらして足りなくなる１桁には無条件に0が入る(下図参照)。

    次に、符号付き整数のシフトを説明する。符号付き整数は、上位1bitが符号を表している。 この符号を壊さないようにシフトさせる(下図参照)。この理由は後述する。

    上図のように符号なし整数の「<<」と符号付き整数の「<<」は同じ働きをする。 しかし「>>」は符号付き整数と負号なし整数の場合で異なる。

    さて、シフトを行うとデータはどうなるのだろうか？まず、左にずらした時の例を示す。

    このように左にずらすと、その値は2倍になる。

    次に、右にずらすことを考える(下図参照)。

    右にずらす場合、符号付き整数と負号なし整数でこのような差がでる。しかし 結果的にはどちらの場合も二分の一になる。


演算子

5-6.キャスト演算子

    キャストとは「型変換」のことです。下の例のように代入元と代入先の変数の型が 異なる場合、型変換を行う必要があります。一般に「char」型から「int」型への変換など、 小さい入れ物から大きな入れ物へは自動的にキャストしてくれる。これを「暗黙の型変換」 という。
   int  i;
   char c = 'A';

   i = c;

    逆に大きな入れ物から小さな入れ物へのキャストは、コンパイラにもよりますが、 データが失われるために警告を出す。このような場合、コンパイラに「データが失われる かもしれないのは、わかっているよ。」と教えなければならない。これを「明示的な型変換」 と言う。
     int  i = 300;
     char c ;

//   c = i;	// 警告が出るかもしれない
     c = (char)i;	// 明示的な型変換

   また、整数と小数(実数)の関係も場合によっては明示的な型変換が必要になります。 下の左の例では明示的な型変換はしていません。右の例では明示的な型変換をしています。 キャストしなければわり算の結果は整数になってしまいます。
#include <stdio.h>

void main(){
    int   a = 300, b = 7;
    float f = a / 7;

       printf("%f\n", f);
}

#include <stdio.h>

void main(){
    int   a = 300, b = 7;
    float f = (float)a / 7;

       printf("%f\n", f);
}

42.000000

42.857143


演算子

5-7.順次演算子

    カンマで区切られたいくつかの式を左から順に実行します。このカンマのことを 順次演算子と言います。
#include <stdio.h>

void main(){
    int a = 0;

    a=a+1, a=a*2, a=a+3;
    printf("a = %d\n", a);
}

a = 5

#include <stdio.h>

void main(){
    int a, b = 5;

    // b*3がaに入る
    a=(a=3, b+=a, b*3);
    printf("a = %d\n", a);
}

a = 24


演算子

5-8.条件演算子

    条件式が「真」の場合、「偽」の場合それぞれに実行される文が短い場合に 用いられる演算子です。
演算子 意　味 使用法
? : 「if～else」文と同じ 式1 ? 式2 : 式3
式1が真なら式2が実行される。
式1が偽なら式3が実行される。
    したがって下の例は同じ働きをする。
    if ( a > 10 )
          b = a;
    else
          b = -a;

    a > 10 ? (b = a) : (b = -a);

    また、この演算子を入れ子にすることもできる。しかしこの演算子の入れ子は、後でバグが出たときに見直したり、共同開発している 他のプログラマーが見ると、とても見にくいので、入れ子にすることはあまりおすすめしない。
    if ( a > 10 ){
        if ( a > 100 )
            b = 2;
        else
            b = 1;
    }
    else
        b = 0;

    a > 10 ? a > 100 ? (b = 2) : (b = 1) : (b = 0);

    // これも、上と同じ
    b = (a > 10 ? a > 100 ? 2 : 1 : 0);
    この演算子の結果は最後に実行された式の値になる。したがって右側の２行目の文では、 a=1000のとき実行されるのは「2」となり、演算子の結果も「2」となる。それが変数bに 代入されるので、最終的には「b = 2」となる。


演算子

5-9.sizeof演算子

    sizeof演算子は、実際に変数がどのくらいの大きさのバイト数を使用しているかを 調べる演算子です。括弧の中は「変数名」もしくは「型」です。 条件式が「真」の場合、「偽」の場合それぞれに実行される文が短い場合に 用いられる演算子です。
#include <stdio.h>

void main(){
    long  a;
    char  b;
    float c;

    printf(" a:%d \n b:%d \n c:%d \n double:%d \n", sizeof(a), sizeof(b), sizeof(c), sizeof(double));
}

 a:4
 b:1
 c:4
 double:8

    sizeof演算子は、配列にも利用できる。要素数10のshort型配列は、「20」という結果が得られる。 これを「sizeof(short)」で割ることで、要素数である「10」を得ることができる。

    例えば配列を宣言し、その中身を0から連続する数字で初期化したい場合は、 今までは左下のように書いてきたが、右下のように書けば配列の大きさが変わっても、 for文を変更する必要がない。

#include <stdio.h>

void main(){
    int  a[20];
    int  i;

    for ( i = 0 ; i < 20 ; i ++ )
        a[i] = i;
}

#include <stdio.h>

void main(){
    int  a[20];
    int  i;

    for ( i = 0 ; i < sizeof(a) / sizeof(int) ; i ++ )
        a[i] = i;
}


演算子

5-10.演算子の優先順位

    今までに紹介した演算子には優先順位があります。例えば、足し算よりもかけ算の方が 優先順位が高いので、かけ算を先に行う等です。同じ優先順位のものは場合によっては 左にある演算子から右に、場合によっては逆に右から左に評価される場合もあります。 例えば、引き算は左にある物から評価されるので、普通の算数の場合と同じになります。

優先順位 | 演算子の種類 | 演算子
↑
高

低
↓ 		式 ()　[]　->※1　.※1
単項演算子 | !　~　++　--　(キャスト)　*※2　&※2　sizeof
二項演算子 剰余 *　/　%
加減 +　-
シフト <<　>>
比較 <　<=　> >=
等値、非等値 ==　!=
論理積(ビット演算子) &
排他的論理和(ビット演算子) ^
論理和(ビット演算子) |
論理積 &&
論理和 ||
条件演算子 ?　:
代入演算子、複合代入演算子 =　+=　-=　*=　/=　%=　<<=　>>=　&=　^=　|=
順次演算子 ,
  ：　同順位の演算子が並んだ場合、左にある物から評価される
  ：　同順位の演算子が並んだ場合、右にある物から評価される

※1　：　後述する構造体・共用体で使われる演算子
※2　：　後述するポインタで使われる演算子

    これらの優先順位はすべて覚える必要はありません。どちらかわからなくなった場合は 括弧で囲めばいいのです。逆に括弧をつけないで書こうとすると、バグの原因になったり、 見直すときに見にくくなります。

    また、論理演算子の「&&」「||」では注意が必要になります。「&&」の場合、 １項目が「偽」になると２項目以降は評価されません。逆に「||」では１項目が「真」に なった時点で２項目以降は評価されません。

    int  a, b, c;

    if ( (a==1) || (b==2) || (c==3) )
c==3が評価されるのは「a!=1」でかつ「b!=2」の時だけです。
    int  a, b, c;

    if ( (a==1) && (b==2) && (c==3) )
c==3が評価されるのは「a==1」でかつ「b==2」の時だけです。
    逆にこの性質を用いると下の２例(aの値が10～20の間だけaの値が表示される)は同じになります。
#include <stdio.h>

void main(){
    int  a;
    scanf("%d", &a);

    (10 <= a) &&
    (a <= 20) &&
    printf("%d\n", a);
}

#include <stdio.h>

void main(){
    int  a;
    scanf("%d", &a);

    if ( 10 <= a && a <= 20)
        printf("%d\n", a);
}
    また、「if文」や「for文」、「while文」などで、「[条件式1]と[条件式2]が[真]なら」 という判断をする場合、[条件式1]と[条件式2]の[偽]になる可能性が高い条件式を 先に記述する方が、処理速度が速くなることを意味しています。
#include <stdio.h>
#include <stdlib.h>

void main(){
    int  a;

    do{
        a = rand();
        printf("%d\n", a);
//	}while( a != 10 && a <= 20000 );
    }while( a <= 20000 && a != 10 );
}
※「rand()」関数はint型の乱数を返す関数です。これを利用するためには「stdlib.h」を インクルードする必要があります。
    上の例は、乱数を取得して、20000以下の数でかつ10でなければ繰り返すという プログラムです。一般的に考えて、「乱数20000以下」という条件と、「乱数が10でない」という 条件を考えた場合、「偽」になる可能性は前者の条件の方が高いです。したがって、 緑文字の「while文」よりも、青文字の「while文」の方が効率的であると言えます。


演算子

5-11.式と値

    C言語では「関数や式も値を持つ」という考え方を持っています。 すなわち「a = a + 1;」という文を考えたとき、変数「a」も、定数「1」も、 演算式「a + 1」も、式「a = a + 1」も値を持っています。 式「a = a + 1」の値は演算式「a + 1」と同じです。

    1章で「「a = b = 100;」は実は複雑な使い方です」と 書きました。前ページの表によると、「=が２つある場合は 右側の=が先に評価される」ことがわかります。したがって、「b = 100」 が最初に評価されます。上で解説したように、「b = 100」の値は [100]になるため、これが変数aに代入されます。その結果、変数aにも変数bにも 100が代入されます。

    では以下のような場合、aには何が代入されるでしょう？

    a = b == c;
    「=」と「==」を比較すると「==」の方が優先順位が高いので、式「b == c」が 評価されます。その結果が変数「a」に代入されます。したがって、bとcが 同じならaに「真(1)」が、bとcが異なれば「偽(0)」が代入されます。

    では、以下のような場合はどうなるでしょう？
#include <stdio.h>

void main(){
    int a = 10, b = 10, c = 10;

    if ( a == b == c )
        printf("Hello\n");
}
    aもbもcも10が代入されているので、「Hello」と表示されそうですが、 [==]が２つあった場合左から評価されます。したがって「a==b」が評価され、同じなら 1、異なるなら0という結果になります。次にその結果とcの値を評価するので、 偽になります。したがって「Hello」とは表示されません。


演算子

5-12.条件式と値

    条件式は「真」か「偽」でプログラムの流れが変わることは説明しました。 「真」というのは「0以外の値」で、「偽」というのは「0と等価」であることも 説明しました。

    したがって、「if文」などの条件式には、「==」や「>=」といった演算子 だけでなくても良いことになります。つまり条件式の中に「a--」とか 「a = a + 1」とか書いても良いことになります。

    以上のことから以下の例のように書き換えることもできます。

    while(a != 0)
       ↓
    while(a)

    while(a == 0)
       ↓
    while(!a)


演算子

5-13. 練習問題

問題
    次の２つのプログラムの実行結果を答えよ。また単項演算子を使用せず、複合代入演算子 を用いて書き換えよ。
#include <stdio.h>

void main(){
    int  i = 0;
    while(i++<10)
        printf("%d\n", i);
}

#include <stdio.h>

void main(){
    int  i = 0;
    while(++i<10)
        printf("%d\n", i);
}


演算子

5-13. 練習問題

問題
    次の２つのプログラムの実行結果を答えよ。また単項演算子を使用せず、複合代入演算子 を用いて書き換えよ。
#include <stdio.h>

void main(){
    int  i = 0;
    while(i++<10)
        printf("%d\n", i);
}

#include <stdio.h>

void main(){
    int  i = 0;
    while(++i<10)
        printf("%d\n", i);
}

解答
1
2
3
4
5
6
7
8
9
10

#include <stdio.h>

void main(){
    int i = 0;
    while ( i < 10 ){
        i+=1;
        printf("%d\n", i);
    }
    i+=1;
}

1
2
3
4
5
6
7
8
9

#include <stdio.h>

void main(){
    int i = 0;
    i+=1;
    while ( i < 10 ){
        printf("%d\n", i);
        i+=1;
    }
}


ポインタ

第6章. ポインタ

6-1.ポインタとは

    プログラムが実行されているとき、プログラムや変数などはメモリー上に格納されています。 すべてのメモリーにはアドレスが振られています。変数にアクセスするときプログラムは このアドレスを指定してアクセスします。アドレスは単なる数字なのでプログラマーにとっては わかりにくい存在です。そこで変数名というプログラマーにわかりやすい名前で 変数にアクセスすることができます。ポインターは変数名ではなく、アドレスで 変数にアクセスするためのものです。

    今まで何気なく変数を定義してきました。変数の定義とは、メモリーに変数を入れる ための領域を確保し、そのアドレスを覚えておくという操作なのです。

    試しにint型の変数を宣言し、それがどの領域(アドレス)に確保されたかを表示する プログラムを示します。

#include <stdio.h>

void main(){
    int  n;
    printf("変数nは%x番地に確保されました\n", &n);
}
    上の例のようにアドレスを知るためには、変数の前に「&」を つけます。

    アドレスで変数にアクセスするためには、アドレスを格納する変数というのが 必要になります。これは何型の変数なのでしょうか？　この変数がポインタと言います。 ポインタはアドレスを入れるための専用の変数です。これは今使っている コンピュータによってサイズが異なります。現在のコンピュータの大半は 32bitのサイズを持っています。

    また、ポインタは色々な種類があり、int型のポインタ、char型のポインタなどが あります。


ポインタ

    ポインタ変数の宣言は下のように、型の後に「*」をつけます。 今までの変数のように、宣言しただけではどんな値が入っているかわかりません。 もしそのアドレスが指すメモリーを読もうとすると、Windowsなどの最近のOSでは エラーが起こり、そのプログラムは強制的に終了されますし、MS-DOSなど 一世代前のOSでは、強制終了は起こらないにしてもどんな値が入っているか、 意味のない値を読んでしまうことになります。

    int *p;
    そのポインタが指すメモリーにアクセスするときには、ポインタ変数の前に 「*」をつけます。以下の例を参考にしてください。
#include <stdio.h>

void main(){
    int  a = 5;
    int* p;

    p = &a;  	// ポインタ変数pに変数aのアドレスを入れる

    printf("変数aの値は%dです。\n", a);
    printf("ポインタ変数pの値は0x%xです。\n", p);
    printf("ポインタ変数pの指すメモリーの中身は%dです。\n\n", *p);

    *p = 10;  	// ポインタ変数の指すメモリーを書き換える
    printf("変数aの値は%dです。\n", a);
    printf("ポインタ変数pの値は0x%xです。\n", p);
    printf("ポインタ変数pの指すメモリーの中身は%dです。\n", *p);
}

変数aの値は5です。
ポインタ変数pの値は0x65fdf4です。
ポインタ変数pの指すメモリーの中身は5です。

変数aの値は10です。
ポインタ変数pの値は0x65fdf4です。
ポインタ変数pの指すメモリーの中身は10です。
    ちょっと不思議な例を示します。
#include <stdio.h>

void main(){
    int  a = 5;
    int* p;

    p = &a;  	// ポインタ変数pに変数aのアドレスを入れる

    printf("ポインタ変数pの値は0x%xです。\n", p);

    p++;
    printf("ポインタ変数pの値は0x%xです。\n", p);
}

ポインタ変数pの値は0x65fdf4です。

ポインタ変数pの値は0x65fdf8です。
    ポインタ変数の値を1つ増やしたのに、ポインタ変数の値は4増えています。 これは、int型は4Bytesで1つの変数を表しているので、4増えるのです。 これについては次のページで説明します。

    さて、この増えたポインタ変数が指すメモリーには何が入っているのでしょう？ これはやってはいけないことです。なぜならば、プログラマーが宣言した変数の 領域を越えてしまうため、そのアドレスは何が入っているかわからないからです。 もしそのメモリーを書き換えると何が起こるかわかりません。


ポインタ

6-2.配列とポインタ

    １つ１つの変数に対して、ポインタをしようするメリットは全然ありません。 ポインタは配列や後述する構造体や 共用体と組み合わせて初めてメリットが生まれるのです。

    添え字を使った場合と、ポインタを使った場合の配列の中身を表示するプログラムを 示します。

#include <stdio.h>

void main(){
    int  a[10] = { 5, 4, 9, 1, 10, 7, 8, 3, 2, 6 };
    int  i;

    for ( i = 0 ; i < 10 ; i++ )
        printf("%d  ", a[i]);
}

#include <stdio.h>

void main(){
    int  a[10] = { 5, 4, 9, 1, 10, 7, 8, 3, 2, 6 };
    int  *p = a;
    int  *pend = a + 10;

    for ( ; p < pend ; p++ )
        printf("%d  ", *p);
}
    実は、配列の変数に対して添え字を使わない形の変数そのもの(ここではa)は その配列の先頭アドレスを指すのです。そして配列とは、メモリー上に連続して 領域が取られるので、アドレスを１つ分(1バイトではない)増やせば、配列の 次の要素が得られるのです。


ポインタ

    これまでのことを整理するためにlong型のポインタとchar型のポインタの違いを 見てみることにします。コンピュータによってポインタの大きさは異なりますが、 ここでは現在主流である32bitと仮定して解説します。

    上図のように、long型ポインタもchar型ポインタも、ポインタは32bit(4byte)変数です。 char型のサイズは8bit(1byte)なので、char型ポインタを１つ進めると、ポインタの値は 1増えます。

    これに対してlong型の場合は、32bit(4byte)の大きさなので、long型ポインタを １つ進めると、ポインタの値は4(byte)増えます。1(byte)とか2(byte)増やすことは 基本的にはできません。char型ポインタにキャストして増やせば1増やすことも 可能ですが意味がありません。

    もし1(byte)とか2(byte)とか増えたら、下図の※の ように、無効な変数へのアクセスをしてしまうことになります。

    次に、ポインタの演算について説明します。これから説明することはコンパイラによって 多少違う点があるかもしれません。

    ポインタはアドレスなので、アドレスの計算に意味がないような演算は許されません。 例えば、ポインタの乗算、除算は、意味がないので許されません。またポインタ同士の 加算も意味がありませんが、減算は要素数を知るために必要なので可能です。 あくまでも要素数になり、バイト数ではないことに注意が必要です。さらに、 ポインタ同士の比較は同じ型のポインタどうしてなければできません。これらを まとめると以下のようになります。

許される演算
  • 同じ型のポインタ同士の減算
  • 同じ型のポインタ同士の比較
  • ポインタと整数の加算
  • ポインタから整数の減算


ポインタ

6-3.文字列とポインタ

    4章で説明した文字列は、char型配列を利用した。 配列でなくポインタを利用しても文字列を使用することができる。
#include <stdio.h>

void main(){
    char *str = "Hello";

    printf("%s\n", str);
}
    ダブルクォーテーションは、囲った文字列のアドレスを返すという意味になる。 したがって上の例では「Hello」という文字列が格納されたアドレスをchar型配列 strに代入するという意味になる。

    しかし下のような使い方はできない。

#include <stdio.h>

void main(){
    char *str;

    scanf("%s", str);
    printf("%s\n", str);
}
    これはcharポインタを宣言しただけでは、実際に文字列を格納する 領域は確保されていないからである。

    文字列の配列を作るのに４章では２次配列を 使用した。しかし２次配列は長い文字列と短い文字列が混在している場合、 長い文字列に領域が合わせられるため、無駄な領域ができる。そこで、 charポインタの配列を作る。こうすると配列はポインタ変数の配列しか 作られないため、無駄がない。

#include <stdio.h>

void main(){
    int  i;
    char *a[5];

    a[0] = "Hello";
    a[1] = "Good morning";
    a[2] = "Good evening";
    a[3] = "Good night";
    a[4] = "Bye Bye";

    for ( i = 0 ; i < ; i++ )
        printf("%s\n", a[i]);
}


ポインタ

6-4.ポインタのメリット

    構造体や共用体が 出てこないと、あまりポインタのメリットは出てこないのですが、配列と 組み合わせた場合のメリットを紹介します。

    下の例は、配列の中身の合計を求めて表示するプログラムです。 配列の終わりには、必ず0が入っていると仮定します。

#include <stdio.h>

void main(){
    int  a[] = {10, 5, 9, 1, 3, 4, 7, 2, 8, 6, 0};
    int  i;
    int  total = 0;

    for ( i = 0 ; a[i] ; i++ )
        total+= a[i];

    printf("total : %d\n", total);
}

#include <stdio.h>

void main(){
    int  a[] = {10, 5, 9, 1, 3, 4, 7, 2, 8, 6, 0};
    int  *p;
    int  total = 0;

    while(*p)
        total+= *(p++);

    printf("total : %d\n", total);
}
    プログラムサイズを見ると、ポインタを使った方が少し小さいくらいです。 しかし実行速度を考えると、ポインタを使った方が断然速いのです。繰り返し処理の 中身を考えてみることにする。
配列の場合
  • 添え字[i]とint型の大きさ(4byte)を乗算し、配列[a]のアドレスを足す
  • そのアドレスの中身を読み出す
  • その値が0かどうかを判断する
  • 添え字[i]とint型の大きさ(4byte)を乗算し、配列[a]のアドレスを足す
  • そのアドレスの中身を読み出す
  • 変数[total]に足し込む
ポインタの場合
  • ポインタの指すメモリーを読む
  • その値が0かどうかを判断する
  • ポインタの指すメモリーを読む
  • 変数[total]に足し込む
  • ポインタを１つ進める
    このように、ポインタを使った方が、計算量が少なくなるのです。 もしこれが膨大な配列に対して行えば、その差は馬鹿にできないくらいの時間になります。 でも今のCPU速いからなぁ


関数

第7章. 関数

    C言語の基本構成単位は関数です。関数の組み合わせによってプログラムが作られています。 C言語には「printf」に代表されるように標準関数という関数が用意されています。 しかし標準関数だけでなくユーザーが関数を作ることもできます。今までのプログラムでは 自分で作った関数は「main」関数だけです。これは処理が単純だったため、main関数１つ だけしか必要なかったからです。(C言語ではmain関数が一番最初に実行されるという決まりがある。) しかしプログラムが大きくなり、処理が複雑になると関数をいくつか作る必要があります。 以下に関数の作り方を説明します。

7-1.関数の作り方

    自分で作る関数は機能単位にします。これは、 プログラムの再利用性を高めるためです。例えば、表示する関数「printf」は 表示する機能をまとめることによって、いろいろなプログラムで使うことができるように なっています。このように、関数を作るときは、他のプログラムで使用するときも 何も変更しないで良いように、ある一つの機能を持たせ、そのプログラム専用にならない ように設計します。

    また、関数の大きさは目安として100行、 長くても200行以内になるようにします。これ以上大きくなるときは、 その関数の機能を見直したり、その関数の中でさらに関数にできる部分を 探し出すなどして、短くするべきです。

    では関数を作る前に「main」関数を見直してみることにします。

void main(){
    int  a;
    int  b;
      :
      :
}
    「main」というのは関数の名前です。関数の名前は前述した 規則を守れば、自由にプログラマーが決めることができます。

    その前の「void」というのは関数の型です。関数も値を持つことは説明しました。 その関数がどのような型の値を持つかを示します。voidというのは、値を持たないと言う 型です。

    「int　a:」というのは、その関数の中で使う変数の宣言です。 このようにある関数の中でしか使わない変数を「ローカル変数 」と言います。ローカル変数は、他の関数内のローカル変数と同じ名前が あっても構いません。(もちろん同じ関数内のローカル変数は名前が同じであってはなりません。) また、ローカル変数が必要なければ、省略することもできます。

    続いて、処理を記述して終わりです。


関数

    では、関数を作るための書式を説明します。
[型] 関数名([引数宣言]){
    [ローカル変数の宣言]

    処理

    [return 式]
}
    [型]は関数の型です。省略すると「int」型になりますが、きちんと宣言するべきです。

    関数名は関数の名前です。前述した規則を守れば、 自由に決めることができます。ただし、同じ名前はダメです。(C++言語では、同じ名前が あっても引数が異なれ命名できる。)関数名は、名前を見ればどんな処理をする関数か すぐわかるように命名するべきです。

    [引数宣言]は引数の宣言で、必要な個数記述します。省略すると引数はないと言うことに なります。

    [ローカル変数の宣言]も必要なければ省略可能です。

    処理は、関数本体です。省略しても良いですが、関数の意味が無くなります。

    [return 式]は関数の型がvoid型の場合は省略できます。returnの後の式が 関数の値になります。式の型と関数の型は同一でなければなりません。


関数

では、実際に足し算を行う関数を作ってみることにします。

型は「int型」にします。
関数名はplusintにします。
引数はint型２つにします。
ローカル変数は、必要ありません
これらの条件から関数を作ってみると、下のようになります。

int plusint(int a, int b){
    return a + b;
}
    ではこの関数を使って、キーボードから２つの整数を入力し、その合計を表示するプログラム を作ってみましょう。
#include <stdio.h>

int plusint(int a, int b){
    return a + b;
}

void main(){
    int  i1, i2;
    int  total;

    scanf("%d %d", &i1, &i2);
    total = plusint(i1, i2);

    printf("%d\n", total);
}


関数

7-2.プロトタイプ宣言

    前ページの「plusint」をもう一度見直してみましょう。
#include <stdio.h>

int plusint(int a, int b){
    return a + b;
}

void main(){
    int  i1, i2;
    int  total;

    scanf("%d %d", &i1, &i2);
    total = plusint(i1, i2);

    printf("%d\n", total);
}
    main関数よりも前にpluint関数を前に書いたのには訳があります。もしこれを逆にして 下のように書くとエラーになります。
#include <stdio.h>

void main(){
    int  i1, i2;
    int  total;

    scanf("%d %d", &i1, &i2);
    total = plusint(i1, i2);

    printf("%d\n", total);
}

int plusint(int a, int b){
    return a + b;
}
    コンパイラは上から下にコンパイルします。main関数の中でplusint関数が使用されていますが、 その部分をコンパイルしている時点では、まだplusintがどんな関数なのかコンパイラは知りません。 そこでコンパイラはエラーを出してしまうのです。

    これを回避するためには、plusint関数の実体をmain関数よりも前に置いてやれば、 main関数内のplusint関数が出てきた時点で、コンパイラはplusintがどんな関数なのか 知っているので、エラーになりません。

    しかし関数を作るたびに、「この関数はどの部分に書けばいいのか」などと 考えていたら面倒でしょうがありません。

    そこでプロトタイプ宣言という宣言をしておくことで このようなことを回避することができます。コンパイラがコンパイルする時に 必要な情報をあらかじめ宣言しておくのです。必要な情報とは、「関数名」、 「関数の型」と「引数の型と数」です。

    プロトタイプ宣言は、下のような書式で記述します。

    [型] 関数名([引数宣言]);
    です。型を省略するとint型になり、引数宣言を省略すると引数なしと言うことになります。

#include <stdio.h>

  int plusint(int, int);
//int plusint(int a, int b);

void main(){
    int  i1, i2;
    int  total;

    scanf("%d %d", &i1, &i2);
    total = plusint(i1, i2);

    printf("%d\n", total);
}

int plusint(int a, int b){
    return a + b;
}
    上の例のように宣言します。コンパイラは引数の個数と型さえわかればいいので、 引数宣言の引数の名前は省略しても構いません。


関数

7-3.配列とポインタ

    配列やポインタを引数として関数に渡すにはどうすればよいでしょう。 実は関数の引数として配列やポインタを渡すとき、呼ばれる関数側では それが配列なのかポインタなのか判断できません。したがって呼び出し元では 配列として宣言した物を、呼び出し側でポインタとして受け取ることもできるし、 逆に呼び出し元ではポインタとして宣言した物を、関数側では配列として 受け取ることもできます。したがって下の例はいずれもエラーにはなりません。
#include <stdio.h>

void test(char *a){
    printf("%x\n", a);
}

void main(){
    char a[10];

    test(a);
}

#include <stdio.h>

void test(char a[]){
    printf("%x\n", a);
}

void main(){
    char *a = "Hello";

    test(a);
}
    前述したように、配列の名前はその配列の開始アドレスを 指します。したがって、関数の宣言で引数をポインタにしても、配列にしてもアドレスを 取ることには変わりないからです。
#include <stdio.h>

void test(char a[]){
    printf("%x\n", a);
}

void main(){
    char c = 'C';

    test(&c);
}
    ポインタでも配列でもアドレスを受け取るわけですから、上の例のように配列を引数に持つ ように宣言していても、普通の変数のアドレスを渡してやれば、エラーになりません。

    ここで気をつけなくてはいけないことがあります。関数の引数で配列を渡す場合は アドレスしかやりとりしないので、関数側では配列の大きさがわからないということです。 下の例はmain関数内で要素数100の配列を宣言し、そのサイズをmain関数と呼び出された 関数内で表示します。

#include <stdio.h>

void size(char a[]){
    printf("呼び出された関数では：%d\n", sizeof(a));
}

void main(){
    char a[100];

    printf("main関数内では：%d\n", sizeof(a));
    size(a);
}

main関数内では：100
呼び出された関数では：4
    上の例でお分かりのように、呼び出された関数ではsizeofの結果「4byte」と 判断しています。これは配列の大きさではなく、アドレスを入れる入れ物（すなわちポインタ）の 大きさを表示しているにすぎません。


関数

7-4.値渡しとアドレス渡し

    関数の中でその引数の値を変えたらどうなるでしょうか？
#include <stdio.h>

void test(int a){
    a++;
    printf("関数の中での変数aの値%d\n", a);
}

void main(){
    int  a = 10;

    printf("main関数の中での変数aの値%d\n", a);
    test(a);
    printf("main関数の中での変数aの値%d\n", a);
}

main関数の中での変数aの値10
関数の中での変数aの値11
main関数の中での変数aの値10
    上記の例のように関数の中で、引数の変数を変更しても、呼び出し元の関数に 戻った時はその変更が無効になります。これは関数を呼ぶときに、その別のメモリーに 引数の値をコピーするからです。したがって、下の例を参照していただければお分かりの通り、 main関数の変数aのアドレスと、呼び出された関数での引数aのアドレスは異なっています。
#include <stdio.h>

void test(int a){
    a++;
    printf("関数の中での変数aの値%d\n", a);
    printf("関数の中での変数aのアドレス%x\n\n", &a);
}

void main(){
    int  a = 10;

    printf("main関数の中での変数aの値%d\n", a);
    printf("main関数の中での変数aのアドレス%x\n\n", &a);
    test(a);
    printf("main関数の中での変数aの値%d\n", a);
    printf("main関数の中での変数aのアドレス%x\n\n", &a);
}

main関数の中での変数aの値10
main関数の中での変数aのアドレス65fdf4

関数の中での変数aの値11
関数の中での変数aのアドレス65fda4

main関数の中での変数aの値10
main関数の中での変数aのアドレス65fdf4
    関数の戻り値は１つであるため、２つ以上の戻り値が欲しい場合は 戻り値だけではどうしようもありません。その場合は引数に値を詰めて貰うしか 方法がありません。しかし上記の例からわかるように、引数に値を詰めて貰うことが できません。そのような場合は引数に、変数のアドレスを渡します。これを 「アドレス渡し」と言います。 これに対し、今まで紹介してきたような変数のコピーを渡す方法を「 値渡し」と言います。
#include <stdio.h>

void set10(int *a, int *b){
    *a = 10;
    *b = 10;
}

void main(){
    int  a = 100;
    int  b = 100;

    printf("a=%d,  b=%d\n", a, b);
    set10(&a, &b);
    printf("a=%d,  b=%d\n", a, b);
}

a=100,  b=100
a=10,  b=10
    前ページで紹介した、ポインタや配列を渡す方法も アドレス渡しになります。したがって、呼び出された関数の中で配列の要素を 変更した場合、呼び出し元の関数に戻ってきたとき、関数内で変更された結果は 変更されたままです。
#include <stdio.h>

// 配列の要素を表示する
void disparray(int *a, int sizeofarray){
    int *end = a + sizeofarray;

    while( a < end )
        printf("%d\t", *(a++));
    printf("\n");
}

// 配列の要素をすべて１増やす
void incarray(int a[], int sizeofarray){
    while ( --sizeofarray >= 0 )
        a[sizeofarray]++;
}

void main(){
    int  a[5] = { 10, 20, 5, 1, -1 };

    disparray(a, 5);
    incarray(a, 5);
    disparray(a, 5);
}

10      20      5       1       -1
11      21      6       2       0


関数

7-5.main関数の引数

    main関数も関数なので引数を持つことができます。main関数の引数とは、 プログラムの引数です。プログラムの引数は、 MS-DOSやUNIXなどのDOSプロンプトから実行ファイル名の後につけるスイッチや ファイル名などです。MS-Windowsなら「ファイル名を指定して実行」から 実行ファイル名の後に渡すことができます。

C:\WINDOWS>format d:
            ↑    ↑引数
            実行ファイル名
    下の例は、main関数の引数を受け取り、表示するプログラムです。
#include <stdio.h>

void main(int argc, char *argv[]){
    int i;

    printf("引数の個数は%d個です\n", argc);

    for ( i = 0 ; i < argc ; i++ )
        printf("%d番目の引数は%sです\n", i, argv[i]);
}
    変数名はもちろん変えても構いませんが、だいたいどの本を見ても、 ソースコードを見ても、この変数名ですね。

    １番目の引数「argc」は引数の個数、２番目の引数「argv」には 実際の引数が文字列として格納されています。もちろん２番目の引数は ポインタのポインタとして受け取ることもできます。上の例をポインタのポインタ で受け取るサンプルを示します。

#include <stdio.h>

void main(int argc, char **argv){
    int i;

    printf("引数の個数は%d個です\n", argc);

    for ( i = 0 ; i < argc ; i++, argv++ )
        printf("%d番目の引数は%sです\n", i, *argv);
}

C:\WINDOWS>TEST.EXE  -a abc.txt
引数の個数は3個です
0番目の引数はB:\C\test2\Debug\test2.exeです
1番目の引数は-aです
2番目の引数はabc.txtです

C:\WINDOWS>
    引数が無かった場合はargcは1で、argv[0]にはプログラム名が絶対パスで 格納されています。したがって実際の引数はargv[1]以降に格納されていることに なります。

    「C>\WINDOWS\TEST.EXE -a abc.txt」と入力した 時のメモリーイメージを示します。


関数

7-6. 練習問題

問題
    次の３つの関数を作れ。
  • strlength：char型ポインタを引数で取り、文字列の長さを戻り値で返す。
  • strcopy：char型ポインタ２つを引数で取り、文字列をコピーする。戻り値は char型のポインタで、コピー先の文字列の最初のアドレスを返す。
  • main関数：scanfで最大30文字までの文字列を入力し、strlength関数を呼び出し その文字数を表示する。別に用意した配列にstrcopyを用いて入力した文字列をコピーし、 それを表示する。


関数

7-6. 練習問題

問題
    次の３つの関数を作れ。
  • strlength：char型ポインタを引数で取り、文字列の長さを戻り値で返す。
  • strcopy：char型ポインタ２つを引数で取り、文字列をコピーする。戻り値は char型のポインタで、コピー先の文字列の最初のアドレスを返す。
  • main関数：scanfで最大30文字までの文字列を入力し、strlength関数を呼び出し その文字数を表示する。別に用意した配列にstrcopyを用いて入力した文字列をコピーし、 それを表示する。
解答
#include

int strlength(char *str){
    int i;
    for ( i = 0 ; *(str++) ; i++ );
    return i;
}

char* strcopy(char* pdest, char* psrc){
    char *ret = pdest;

    do{
        *pdest = *psrc;
        pdest++;
        psrc++;
    }while(*(psrc-1));
    return ret;
}

void main(){
    char a[31];
    char b[31];

    scanf("%30s", a);

    strcopy(b, a);
    printf("入力された文字数は%d文字です\n", strlength(a));
    printf("入力された文字列は[%s]です\n", b);
}


関数

7-7.標準関数

    ここでは標準関数のうち、文字分類、文字変換、文字列操作、データ変換に関する 標準関数を紹介します。もちろんすべてを覚える必要はありません。試験に出る訳じゃ ないし・・・。もし必要になったときに、「こんな関数あったなぁ」とか、「この 関数はここに書いてあったなぁ」とか覚えて置いてくれれば結構です。

    ここで紹介する関数を使用するには「<ctype.h>」を インクルードする必要があります。

文字分類、文字変換
関数名 | 書　式 | 戻り値 | 機　能
isalnum int isalnum(int ch); 0＝アルファベットもしくは数字ではない
0≠アルファベットもしくは数字 		アルファベットもしくは数字かどうかを調べる
isalpha int isalpha(int ch); 0＝アルファベットではない
0≠アルファベット 		アルファベットどうかを調べる
isascii int isascii(int ch); 0＝ASCII文字ではない
0≠ASCII文字 		ASCII文字(0x00～0x7F)かどうかを調べる
iscntrl int iscntrl(int ch); 0＝制御文字ではない
0≠制御文字 		制御文字(0x00～0x1F)かどうかを調べる
isdigit int isdigit(int ch); 0＝数字ではない
0≠数字 		数字('0'～'9')かどうかを調べる
islower int islower(int ch); 0＝小文字ではない
0≠小文字 		小文字('a'～'z')かどうかを調べる
isprint int isprint(int ch); 0＝表示可能文字ではない
0≠表示可能文字ではない 		ディスプレイ(プリンタ)に表示可能な文字かどうかを調べる
ispunct int ispunct(int ch); 0＝句読点文字ではない
0≠句読点文字 		句読点(ピリオド、カンマなど)かどうかを調べる
isspace int isspace(int ch); 0＝スペースではない
0≠スペース 		スペース(タブやスペース)かどうかを調べる
isupper int isupper(int ch); 0＝大文字ではない
0≠大文字 		大文字('A'～'Z')かどうかを調べる
isxdigit int isxdigit(int ch); 0＝16進数値ではない
0≠16進数値 		16進数値(数字か'a'～'f'、'A'～'F')かどうかを調べる
toascii | int toascii(int ch); | ASCII文字を返す | ASCII文字に変換する
tolower | int tolower(int ch); | 小文字を返す | 大文字のアルファベットなら小文字に変換する
toupper | int toupper(int ch); | 大文字を返す | 小文字のアルファベットなら大文字に変換する


関数

    ここでは、文字列を操作したり、比較などを行う関数を紹介します。

    ここで紹介する関数を使用するには「<string.h>」を インクルードする必要があります。

文字列操作関数
関数名 | 書 式 | 戻り値 | 機 能
strcat | char* strcat( char* p1, char* p2 ); | p1の値がそのまま返る | 文字列p1の後に文字列p2をつなげる
strcmp | int strcmp( char* p1, char* p2 ); | 負・・・文字列p1が文字列p2より小さい ０・・・２つの文字列は同じ 正・・・文字列p1が文字列p2より大きい | 文字列を比較する
strcpy | char* strcpy( char* p1, char* p2 ); | p1の値がそのまま返る | 文字列p2をp1が示すポインタにNULL文字を含めてコピーする
strlen | int strlen( char* p1 ); | 文字列p1の文字数を返す | 文字列の文字数を調べる
strncat | char* strncat( char* p1, char* p2, unsigned int n ); | p1の値がそのまま返る | p1にp2のn文字目までつなげNULL文字を付加する。 文字列p2の長さがnよりも長い場合はp2の長さとする
strncmp | int strncmp( char* p1, char* p2, unsigned int n ); | 負・・・文字列p1が文字列p2より小さい ０・・・２つの文字列は同じ 正・・・文字列p1が文字列p2より大きい | 文字列をn文字目まで比較する
strncpy | char* strcpy( char* p1, char* p2, unsigned int n ); | p1の値がそのまま返る | p2のn文字目までをp1が示すポインタの位置にコピーする ただしNULL文字はつけない


関数

    ここでは、文字列から数値、逆に数値から文字列への変換を行う関数を紹介します。

    ここで紹介する関数を使用するには「<stdlib.h>」を インクルードする必要があります。

データ変換関数
関数名 | 書 式 | 戻り値 | 機 能
atof | double atof( char* p ); | double型に変換した値 | 文字列をdouble型に変換する
atoi | int atoi( char* p ); | int型に変換した値 | 文字列をint型に変換する
atol | lon atol( char* p ); | long型に変換した値 | 文字列をlong型に変換する
itoa | char* itoa( int n, char* p, int d ); | pの値がそのまま返る | 整数nをd進数の文字列に変換する
ltoa | char* ltoa( long n, char* p, int d ); | pの値がそのまま返る | 整数nをd進数の文字列に変換する
sscanf | int sscanf( const char* p1, const char* p2, ... ); | 入力項目数 | 文字列p1を入力元としてscanfと同じ書式でデータ変換を行う
sprintf | int sprintf( char* p1, const char* p2, ... ); | 出力文字数 | printfと同じ書式でディスプレイの代わりに文字列p1として出力する


関数

7-8.再帰関数

    再帰関数とは、ある関数が自分自身を呼び出すことです。

    例として、階乗の計算をする関数を紹介します。階乗とは次のような計算です。

   calc(n) = n * (n-1) * (n-2) * ・・・ * 1
    この計算式は、次のように書き換えることも可能です。

   calc(n) = n * calc(n-1)
   ただし、calc(1) = 1
    では、階乗を計算する関数を作ってみましょう。
#include <stdio.h>

//
// 普通の関数
// calc(n) = n * (n-1) * (n-2) * ・・・ * 1
unsigned int calc(unsigned int n){
    unsigned int ans = 1;
    for ( ; n > 1 ; n-- )
        ans*=n;
    return ans;
}

//
// 再帰関数
// calc(n) = n * calc(n-1)
unsigned int calc2(unsigned int n){
    if ( n <= 1 )
        return 1;
    return n * calc2(n-1);
}

void main(){
    unsigned int n = 5;

    printf("%d! = %d\n", n, calc(n));
    printf("%d! = %d\n", n, calc2(n));
}
    C言語では関数を呼び出すときに、情報をスタックというメモリーに保存します。 そして関数から戻ってくる時にスタックから情報を返して貰います。したがって、 関数を呼ぶ度にメモリーが使用されます。(当然関数から戻ってきたら、メモリーは 解放されます。)ということは、再帰関数で自分自身を呼ぶことを大量に行うと メモリーが足らなくなるか、壊しては行けない領域を壊して、暴走したりします。 試しに、上のプログラムのmain関数の「n」の値を５から100000くらいにしてみて ください。関数calcは時間が経てば終了しますが(結果は保証されませんが)、関数 calc2の方は、強制的にプログラムが終了されたり、暴走したりすると思います。 必要によって、チェックする機構をつけるべきでしょう。


プリプロセッサ

第8章.プリプロセッサ

8-1.プリプロセッサとは

    プリプロセッサとは、コンパイルの前に行う処理や関数、変数や関数の 宣言を行ったりする物です。

    今までプログラムの最初に「#include <stdio.h>」という 行を書いてきました。これがプリプロセッサと呼ぶものです。この意味は 後述します。

    C言語では好きなフォーマット(フリーフォーマット)で書くことができます。 しかし、このプリプロセッサはだけはフォーマットが決まっています。その フォーマットとは以下の通りです。

  • 「#」と予約語で始める
  • 行の終わりには「;」は書かない
  • 行の途中で改行したい場合は「\」を記述してから改行する
  • 一般に「字下げ」しない
    では、具体的な例を紹介します。


プリプロセッサ

8-2.#define, #undef

    #defineは文字列を他の文字列等に変換する働きを持ちます。書式は 以下のようになります。

  #define  文字列1  文字列2

  例)
     #define  SIZE  10
    「#define」と「文字列1」、「文字列1」と「文字列2」の間には 少なくとも１つのタブもしくはスペースを記述します。上の例では、「SIZE」 とプログラム中に書くと、「10」に置き換えてコンパイルします。 したがって、下の２つのプログラムは全く同じ物になります。
#include <stdio.h>
#define SIZE 10

void main(){
    char buffer[SIZE];
    int  i;

    for ( i = 0 ; i < SIZE ; i++ )
        buffer[i] = 0;
}

#include <stdio.h>

void main(){
    char buffer[10];
    int  i;

    for ( i = 0 ; i < 10 ; i++ )
        buffer[i] = 0;
}
    「SIZE」は変数ではありません。コンパイルしている最中に、「SIZE」 という文字列があったら、「10」と置き換えろと言う意味になります。この 「10」というのはコンパイラにとっては、数字なのか、文字列なのか知る必要が ないのです。

    上の例で、配列の要素数を変更したいと思った場合、「#define」を使用すれば、 その「#define」の値だけを変更するだけですみます。したがって、「#define」 を使用することで、プログラムの保守性があがることになります。

    また「文字列2」は式を書くこともできます。

#include <stdio.h>
#define SIZE       10
#define SIZESQUARE (SIZE * SIZE)

void main(){
    char buffer[SIZE];
    int  i;

    for ( i = 0 ; i < SIZE ; i++ )
        buffer[i] = SIZESQUARE;
}
    この場合、優先順位の問題から、括弧で囲む方が安全です。

    また、#defineの文字列1は、一般に大文字で記入します。 これはそうしなければいけないと言うわけではないのですが、 普通の変数と区別するためです。

    なお、一度定義したものを別の物に置き換えることはできません。

#define ABC "abc"
#define ABC "ABC"     ←これはダメ
    もし、別の物に定義しなおしたい場合は、一度定義を無効にする必要があります。 それには「#undef」を使用します。
#define ABC "abc"
#undef  ABC
#define ABC "ABC"     ←これはOK


プリプロセッサ

8-3.#include

    #includeは他のファイルを読み込みます。読み込むファイルは 主に「*.h」という拡張子が「h」のファイルです。このようなファイルを ヘッダーファイルといいます。

    ヘッダーファイルには、関数の宣言や、前ページで 紹介した、「#define」が書かれています。もちろんこのようなヘッダー ファイルを自分で作ることもできます。これについては後述します。

    書式は以下の通りです。

  #include   <ファイル名>
         もしくは
  #include   "ファイル名"

  例)
     #include  <stdio.h>
    「<>」で囲む場合と、「""」で囲む場合では意味が異なります。「<>」で囲む場合は コンパイラが用意してくれるヘッダーファイルです。例えば標準関数の宣言をしている ヘッダーファイルを読み込む場合などで、コンパイラが読み込むファイルの場所を自動的に 探してくれます。

    それに対して「""」で囲む場合は、主に自分で作ったヘッダーファイルを読み込む場合です。 この場合、コンパイラはファイルの場所を検索してくれませんので、これを記述している ソースファイル(*.Cファイル)があるディレクトリからの相対パス、もしくは絶対パスで (必要ならば)指定する必要があります。もしディレクトリを省略した場合、ソースファイルと 同じディレクトリにあると言うことになります。


プリプロセッサ

トップ-> C言語入門：プリプロセッサ-> 8-4. #if
8-4.#if

    #ifは下記のような書式で記述し、その範囲をコンパイルしたり、しなかったり する場合に使用します。青の部分は省略可能な部分です。

  #if 1
      // コンパイルされる部分
      printf("ここはコンパイルされます\n");
  #else
      // コンパイルされない部分
      printf("ここはコンパイルされません\n");
  #endif

  #if 0
      // コンパイルされない部分
      printf("ここはコンパイルされません\n");
  #else
      // コンパイルされる部分
      printf("ここはコンパイルされます\n");
  #endif
    このプリプロセッサには似たような物がたくさん用意されています。 「#define」と組み合わせて、そこで何が定義されているかどうかを 判断してコンパイルするかどうかを決めることもできます。
  #define TEST 1
  #if (TEST == 1)
      // コンパイルされる部分
      printf("ここはコンパイルされます\n");
  #else
      // コンパイルされない部分
      printf("ここはコンパイルされません\n");
  #endif

  #define TEST 2
  #if (TEST == 1)
      // コンパイルされない部分
      printf("ここはコンパイルされません\n");
  #else
      // コンパイルされる部分
      printf("ここはコンパイルされます\n");
  #endif
    また、「#define」で定義されている値を調べるのではなく、定義されているかどうか だけで決めることもできます。
  #define TEST 1
  #ifdef TEST
      // コンパイルされる部分
      printf("ここはコンパイルされます\n");
  #else
      // コンパイルされない部分
      printf("ここはコンパイルされません\n");
  #endif

  #ifdef TEST
      // コンパイルされない部分
      printf("ここはコンパイルされません\n");
  #else
      // コンパイルされる部分
      printf("ここはコンパイルされます\n");
  #endif
    上の例では、定義されていたらコンパイルしていましたが、定義されていなかったら コンパイルすることもできます。
  #ifndef TEST
      // コンパイルされる部分
      printf("ここはコンパイルされます\n");
  #else
      // コンパイルされない部分
      printf("ここはコンパイルされません\n");
  #endif

  #define TEST 1
  #ifndef TEST
      // コンパイルされない部分
      printf("ここはコンパイルされません\n");
  #else
      // コンパイルされる部分
      printf("ここはコンパイルされます\n");
  #endif
    これらは、例えば英語版と日本語版の両方のプログラムを作っている場合、 プログラムの一番最初に「#define ENGLISH 1」を 定義しておき、実際にメッセージ表示をしている部分に、「#if ENGLISH == 1」 などと、コンパイルする部分を決める場合などに使用します。
← 前ページへ　:　 トップへ　:　 次ページへ →


プリプロセッサ

8-5.#error、#warning

    #error、#warningは強制的にコンパイルエラーを起こしたり、 警告を表示したりします。書式は次の通りです。

  #error   エラーメッセージ
  #warning 警告メッセージ
    これらのプリプロセッサは、前ページの#ifと 組み合わせて使われることが多いです。例えば、以下の通りです。
  #define TEST 1
  #ifdef TEST
      // コンパイルされる部分
      printf("ここはコンパイルされます\n");
  #else
  #error TESTを#defineしてください
  #endif
    警告は、まだ完全にできていない部分に記述することもあります。
  void func(){
  #warning  ここは完全に完成していません
      :
      :
  }


プリプロセッサ

8-6.マクロ

    マクロとは、#defineで作った簡単な関数のようなものです。 例えば、１行とか２行で作れるような関数をマクロにします。簡単な関数を 作り、もしそれが頻繁に呼ばれるような場合、処理が遅くなります。

    マクロですと、コンパイル時に文字列が置き換えられるだけのため、 関数のように、処理がそちらに飛んだりする事がなくなるため、処理速度は 向上します。

    では実際の関数とマクロの定義の仕方を紹介します。ここでは半径を 引数として受け取り、その面積を返す関数です。

#include <stdio.h>
#define PI 3.1415

double  menseki(int r){
    return r * r * PI;
}

void main(){
    int hankei = 10;
    printf("半径%dの面積は%fです\n", hankei, menseki(hankei));
}

#include <stdio.h>
#define PI 3.1415
#define MENSEKI(r) ( r * r * PI)
void main(){
    int hankei = 10;
    printf("半径%dの面積は%fです\n", hankei, MENSEKI(hankei));
}
    関数では、引数や戻り値の型は厳密に定義されています。したがって、半径を int型ではなく、double型にしようと思った場合、関数自体を変えなければなりません。 しかしマクロは、基本的に文字列の置き換えなので、マクロ自体には変更は一切 必要ありません。以下に半径をdouble型にしたプログラムを紹介します。 赤字のところが変更した部分です。
#include <stdio.h>
#define PI 3.1415

double  menseki(double r){
    return r * r * PI;
}

void main(){
    double hankei = 10;
    printf("半径%dの面積は%fです\n", hankei, menseki(hankei));
}

#include <stdio.h>
#define PI 3.1415
#define MENSEKI(r) ( r * r * PI)
void main(){
    double hankei = 10;
    printf("半径%dの面積は%fです\n", hankei, MENSEKI(hankei));
}
    では書式を紹介します。

   #define 文字列1(引数)  内容


プリプロセッサ

8-7.定義済みマクロ

    次の表のような定義済みのマクロがあります。
マクロ名 | 内　容
__LINE__ | ソースファイルの行番号(10進数)
__FILE__ | ソースファイル名
__DATE__ | コンパイルされた日付
__TIME__ | コンパイルされた時間
    以下に例を示します。
#include <stdio.h>

void main(){
    int  n = 0 ;

    printf("コンパイルされた日は%sです\n", __DATE__);
    printf("コンパイルされた時間は%sです\n\n", __TIME__);

    printf("1から10を入力してください : ");
    scanf("%d", &n);

    if ( 0 < n && n <= 10 )
        printf("正常です\n");
    else
        printf("エラーが発生しました。エラー場所は%sの%d行目です\n", __FILE__, __LINE__);
}

コンパイルされた日はAug  5 2000です
コンパイルされた時間は22:56:39です

1から10を入力してください : 0
エラーが発生しました。エラー場所はD:\C\test\test.cの15行目です
    上の例のように、「__FILE__」と「__LINE__」は、エラーが出たときに場所を特定 しやすいように表示させるという使い方をするのが一般的です。


変数の有効範囲

第9章.変数の有効範囲

9-1.変数の種類

    変数を作るときは、型の他にも種類があります。記憶クラスと有効範囲です。 ここでは記憶クラスについて説明します。

    記憶クラスとは変数をどこのメモリーに確保するかを決定することです。 その種類は以下の３種類があります。

種類 | 意　味
auto | 一時的にメモリーに確保
static | メモリーに確保、一度割り当てられるとプログラムが終了するまで同じ領域に確保される
register | CPUのレジスターに確保
auto
    auto変数は、今まで出てきた普通の変数です。一般に関数の最初で宣言され、 「auto」を省略しても構いません。宣言する場所は一般に関数の最初ですが、 ブロック(中括弧で囲うブロック)の最初でも構いません。

    auto変数はその有効範囲からはずれると、メモリーから解放されるため、 次に確保されたときにはどんな値が入っているかわかりません。

static
    スタティック変数の宣言する場所は、関数の外か、auto変数と同様ブロックの最初 です。関数の外で宣言されるとグローバル変数と呼ばれ、そのソースファイルの中ならば どこからでも参照することができます。ブロックの最初で宣言した場合は有効範囲は auto変数と同じですが、有効範囲から外に出ても、メモリーは解放されません。 したがって同じブロックに再度は行って来た場合は、前の値がそのまま残っています。

register
    変数をCPUのレジスタに取るように指示します。CPUのレジスタは、メモリーよりも 高速に入出力できるので、頻繁に使われるような変数をレジスタ変数にします。

    この変数はメモリーに確保しないので、「&」をつけて、アドレスを取ることはできません。

    また、CPUによってレジスタの数が異なるので、あまり使用しません。レジスタの個数を 越えて、レジスタ変数を作ろうとした場合、普通のauto変数になります。


変数の有効範囲

9-2.グローバル変数の有効範囲

    グローバル変数とは、下記のように関数の外の部分で宣言した変数です。 グローバル変数は、初期化をしなくても0が代入され、プログラム開始から終了まで、 同じメモリー領域に確保されています。
#include <stdio.h>

int g;

void main(){
    printf("%d\n", g);
}
    グローバル変数の有効範囲(スコープ)は基本的に、そのプログラムの全ての場所です。 したがってmain関数はもちろん、たの関数内でも使用できます。しかしstatic 修飾子をつけると、同じソースファイル内のみになります。

    他のソースファイルで宣言されているグローバル変数を参照するためには、 そのソースファイル内でも同じ宣言をしなければなりません。ただし、extern 修飾子をつける必要があります。この修飾子は他のファイルで宣言されていることを 示します。

#include <stdio.h>

extern int g;

void main(){
    printf("%d\n", g);
}

int g;


変数の有効範囲

9-3.オート変数の有効範囲

    オート変数は、今までのサンプルに出てきたように、関数の最初やブロックの最初で 宣言する変数です。スコープ(宣言した関数やブロック)の外に出ると、メモリーが解放 されるために、次回に同じ関数やブロックに入った場合は、前の値を保持していない。 これを保持させたい場合は、スタティック変数にする。

    スコープは宣言した関数内あるいはブロック内のみである。グローバル変数に 同じ変数名がある場合は、オート変数が優先される。また、下のサンプルのように、 ブロックが入れ子になっている場合も同じ名前の変数を作ることができる。この場合、 現在のブロックで宣言されている変数が優先される。

i(Block3) ---> 1
i(Block3) ---> 1
i(Block3) ---> 1
i(Block2) ---> 3
i(Block3) ---> 1
i(Block3) ---> 1
i(Block3) ---> 1
i(Block2) ---> 3
i(Block1) ---> 2
    上のサンプルでは、全部で変数iが３つ宣言されている。これら３つの変数iは 変数名は同じであるが、実際には異なる変数と見なすことができる。これらは色分けしたところで 異なる変数iが参照されているため、このような出力結果となる。

    ３つめのブロックの変数iは0で初期化されている。この初期化はそのブロックに入る度に 行われるため、ブロック３での出力はすべて「１」となっている。


変数の有効範囲

9-4.スタティック変数の有効範囲

    スタティック変数は、関数の最初あるいはブロックの最初で「static」修飾子を つけて宣言した変数です。スタティック変数は一度メモリーに割り当てられると、プログラムが 終了するまで同じメモリーに割り当てられ、前回の値をプログラム終了まで保持しています。

    それ以外のスコープははオート変数と同じですが、 初期化値がある場合は注意が必要です。オート変数の初期化は、そのブロックに入る 度に行われますが、スタティック変数では初期化は最初の１回だけです。初期化のない スタティック変数は０が入ります。

#include <stdio.h>

void func(){
    static i = 10;

    printf("i=%d\n", i);
    i+= 10;
}
void main(){
    int i;
    for ( i = 0 ; i < 3 ; i++ )
        func();
}

i=10
i=20
i=30


構造体

第10章.構造体

10-1.構造体とは

    int型やchar型はあらかじめC言語で定義されているデータ型です。あらかじめ用意されている データ型を複数あわせて新しいデータ型として、プログラマーが定義したデータ型を 「構造体」と言います。

    構造体はよく名簿にたとえられます。例えば「名前」「電話番号」「住所」からなる 名簿を考えたとき、「名前」「電話番号」「住所」はそれぞれ文字配列として 定義できます。しかし、３つの要素が全てあわさって、１人分のデータになります。 そしてその３つの要素からなる１人分のデータが複数あわさって、はじめて名簿になります。 つまり３つの要素と考えるよりも、１人分のデータを１つの要素と考えた方が 自然ですし、便利です。このように１人分のデータ(複数の要素)を１つの要素と みなすデータ型を構造体と言います。

    次のページからは、実際にこのような名簿の構造体を作ってみることにします。


構造体

10-2.構造体の宣言

    int型はコンパイラが「32bit(16bit)の領域を使用する整数型」ということを 知っています。しかし構造体はプログラマーが定義するデータ型なので、 コンパイラにどんなデータ型なのかを知らせる必要があります。これが 構造体の宣言です。宣言をするためには以下のように記述します。

    struct 構造体データ型の名前{
        メンバーの宣言１
        メンバーの宣言２
        メンバーの宣言３
            ：
            ：
    };

例）
    struct meibo{
        char name[20];
        char tel[11];
        char address[30];
    };
    一般に構造体の宣言は、ヘッダーファイルを作って、そこに記述します。 ヘッダーファイルを作らない場合は、ソースファイルの一番上に記述します。 宣言よりも上では、その構造体を使用することはできません。

    また、上記のような宣言だけでは、コンパイラにデータ型を知らせるだけなので、 実際にメモリーに割り当てられるわけではありません。実際に使用するときは 今までのint型のように変数の宣言をしなければなりません。これは今までの 変数の宣言と全く同じです。

struct meibo{
    char name[20];
    char tel[11];
    char address[30];
};

void main(){
    struct meibo	abe;
    struct meibo	sasaki;

    struct meibo	classmate[30];
}
    上述のように、宣言した構造体を定義するには、「struct」の後に 構造体名を記述し、変数名を指定します。当然ながら、配列も可能です。


構造体

    余り使われませんが、宣言と定義を同時に行うこともできます。
    struct meibo{
        char name[20];
        char tel[11];
        char address[30];
    } abe;

    struct meibo sasaki;
    上の例では、「meibo」という構造体を宣言し、同時にその構造体を使用するために 「abe」という変数としてメモリーを確保しています。次に「meibo」構造体として、 「sasaki」という変数を定義しています。

    上の例の「meibo」という構造体のデータ型名は省略することも可能です。しかし 省略した場合、構造体のデータ型名がないため、変数「sasaki」を定義することはできません。

    さらに普通の変数と同様に、変数の定義と同時に初期化をすることもできます。

    struct meibo{
        char name[20];
        char tel[11];
        char address[30];
    };

    meibo abe = { "abe natsuni", "012-3456-7890", "hokkaido uncharakanchara 1-2-3" };
    次に配列の場合の初期化の例を紹介します。
    struct meibo{
        char name[20];
        char tel[15];
        char address[40];
    };

    struct meibo classmate[] = {{ "abe natsuni"  , "012-3456-7890", "hokkaido uncharakanchara 1-2-3" },
                                { "gotoh mai"    , "0987-654-3210", "tokyo nancharakanchara 9-8-7"   },
                                { "nakazava yuko", "090-1234-5678", "kanagawa hogehoge 1234-5678"    }};


構造体

    次に構造体のメモリーイメージについて説明します。

    基本的に、構造体は配列と同じように連続したメモリーが確保されます。

struct meibo{
    char name[20];
    char tel[11];
    char address[30];
};
    そして、構造体の配列は、上図のような構造体がさらに連続してメモリー上に確保されます。

    メモリーの確保の仕方は基本的には、上記のようなメモリーが確保されます。 しかし詳しい内容はコンパイラによって異なります。著者の環境では、下のような 「int」「char」「int」型の構造体を作ると、右下のようにゴミの領域が作られます。

struct test{
    int  a;
    char b;
    int  c;
};
    これはコンパイラが数値領域を確保する際に、２あるいは４の倍数になるように 確保していくためです。その理由はコンパイラがC言語からコンピュータの唯一理解できる マシン語に変換する際のアルゴリズムに関係します。詳しいことは省略します。

    したがって、構造体の大きさを知りたい場合は、「sizeof演算子」 を使用する必要があります。


構造体

10-3.構造体の使用法

    いよいよ構造体の使い方です。構造体ののメンバーにアクセスするための２つの 演算子が用意されています。「.」と「->(アロー演算子)」です。 アロー演算子は、構造体のポインタとともに使用します。これについては 後述します。

    前者の「.」演算子を用いた構造体メンバーのアクセスについて例を示します。

#include <stdio.h>

struct meibo{
    char name[20];
    char tel[15];
    char address[40];
};

void main(){
    struct meibo classmate[] = {
        { "abe natsuni"  , "012-3456-7890", "hokkaido uncharakanchara 1-2-3" },
        { "gotoh mai"    , "0987-654-3210", "tokyo nancharakanchara 9-8-7"   },
        { "nakazava yuko", "090-1234-5678", "kanagawa hogehoge 1234-5678"    }
    };

    int size = sizeof(classmate) / sizeof(struct meibo);
    int i;

    for ( i = 0 ; i < size ; i++ ){
        printf("No. %d\n", i+1);
        printf("name : %s\n", classmate[i].name);
        printf("tel  : %s\n", classmate[i].tel);
        printf("address : %s\n\n", classmate[i].address);
    }
}

No. 1
name : abe natsuni
tel  : 012-3456-7890
address : hokkaido uncharakanchara 1-2-3

No. 2
name : gotoh mai
tel  : 0987-654-3210
address : tokyo nancharakanchara 9-8-7

No. 3
name : nakazava yuko
tel  : 090-1234-5678
address : kanagawa hogehoge 1234-5678
    では、上記のプログラムに次の修正を加えてみます。１つめは、表示する部分を 別の関数にすることで、２つめは、「gotoh mai」を「gotoh aki」に変更すること です。
#include <stdio.h>
#include <string.h>

// 構造体の宣言
struct meibo{
    char name[20];
    char tel[15];
    char address[40];
};

// 名簿を１件分表示する関数のプロトタイプ宣言
void disp(struct meibo);

//
// メイン関数
void main(){
    struct meibo classmate[] = {
        { "abe natsuni"  , "012-3456-7890", "hokkaido uncharakanchara 1-2-3" },
        { "gotoh mai"    , "0987-654-3210", "tokyo nancharakanchara 9-8-7"   },
        { "nakazava yuko", "090-1234-5678", "kanagawa hogehoge 1234-5678"    }
    };

    int i, size = sizeof(classmate) / sizeof(struct meibo);

    // 名前を変更
    strcpy(classmate[1].name, "gotoh aki");

    // 名簿表示
    for ( i = 0 ; i < size ; i++ )
        disp(classmate[i]);
}

//
// 名簿を１件分表示する関数
void disp(struct meibo person){
    printf("name : %s\n", person.name);
    printf("tel  : %s\n", person.tel);
    printf("address : %s\n\n", person.address);
}

name : abe natsuni
tel  : 012-3456-7890
address : hokkaido uncharakanchara 1-2-3

name : gotoh aki
tel  : 0987-654-3210
address : tokyo nancharakanchara 9-8-7

name : nakazava yuko
tel  : 090-1234-5678
address : kanagawa hogehoge 1234-5678


構造体

    次に構造体のコピーの仕方について説明します。今までのint型変数の代入と同じように 「=演算子」を使用します。この演算子は構造体の全てのメンバーをコピーします。

    下の例では、構造体の配列の順番を入れ換える例です。

#include <stdio.h>
#include <string.h>

// 構造体の宣言
struct meibo{
    char name[20];
    char tel[15];
    char address[40];
};

// 名簿を１件分表示する関数のプロトタイプ宣言
void disp(struct meibo);

//
// メイン関数
void main(){
    struct meibo classmate[] = {
        { "abe natsuni"  , "012-3456-7890", "hokkaido uncharakanchara 1-2-3" },
        { "gotoh mai"    , "0987-654-3210", "tokyo nancharakanchara 9-8-7"   },
        { "nakazava yuko", "090-1234-5678", "kanagawa hogehoge 1234-5678"    }
    };

    struct meibo tmp;
    int i, size = sizeof(classmate) / sizeof(struct meibo);

    // 順番を入れ換える
    tmp = classmate[0];
    classmate[0] = classmate[2];
    classmate[2] = tmp;

    // 名簿表示
    for ( i = 0 ; i < size ; i++ )
        disp(classmate[i]);
}

//
// 名簿を１件分表示する関数
void disp(struct meibo person){
    printf("name : %s\n", person.name);
    printf("tel  : %s\n", person.tel);
    printf("address : %s\n\n", person.address);
}

name : nakazava yuko
tel  : 090-1234-5678
address : kanagawa hogehoge 1234-5678

name : gotoh mai
tel  : 0987-654-3210
address : tokyo nancharakanchara 9-8-7

name : abe natsuni
tel  : 012-3456-7890
address : hokkaido uncharakanchara 1-2-3
    =演算子はすべてのメンバーをコピーします。構造体のメンバーにポインタが 含まれている場合には注意が必要です。=演算子を使用して、構造体をコピーした場合、 ポインタはコピー元とコピー先で同じアドレスを指しますので、どちらか一方のポインタの指す データを書き換えた場合は、もう一方のデータも書き換えられることになります。
#include

struct x{
    int   num;
    char *str;
};

// 構造体の数字と、文字列を表示する
void disp(struct x data){
    printf("%d, %s\n", data.num, data.str);
}

void main(){
    char str[] = "abcde";
    struct x data1 = { 1, str };
    struct x data2;

    // data1 を data2 にコピーする
    data2 = data1;

    // data1, data2の内容を表示する
    disp(data1);
    disp(data2);

    // data1のstrの１文字目を'A'に変更する
    *(data1.str) = 'A';

    // data1, data2の内容を表示する
    disp(data1);
    disp(data2);
}

1, abcde
1, abcde
1, Abcde
1, Abcde
    上の例では、data1の文字を変えただけなのに、data2の内容も変更されています。 これはdata1とdata2の両方が同じアドレスを指しているからです。


構造体

10-4.構造体のポインタ

    int型など既存のデータ型にはポインタが使用できました。 構造体のポインタを使用することもできます。

    ポインタの場合、メンバーにアクセスする場合は「->」演算子を使用します。

    ポインタを使ったプログラムの例として、名簿の内容を表示するプログラムを示します。

#include
#include

// 構造体の宣言
struct meibo{
    char name[20];
    char tel[15];
    char address[40];
};

// 表示関数のプロトタイプ宣言
void dispall(struct meibo*);

//
// メイン関数
void main(){
    struct meibo classmate[] = {
        { "abe natsuni"  , "012-3456-7890", "hokkaido uncharakanchara 1-2-3" },
        { "gotoh mai"    , "0987-654-3210", "tokyo nancharakanchara 9-8-7"   },
        { "nakazava yuko", "090-1234-5678", "kanagawa hogehoge 1234-5678"    },
        { "", "", "" }
    };

    // すべて表示する
    dispall(classmate);
}

//
// 表示関数
void dispall(struct meibo *p){
    // 名前がない場合は終了する
    while( p->name[0] != '\0' ){
        printf("name : %s\n", p->name);
        printf("tel  : %s\n", p->tel);
        printf("address : %s\n\n", p->address);

        p++;
    }
}

name : abe natsuni
tel  : 012-3456-7890
address : hokkaido uncharakanchara 1-2-3

name : gotoh mai
tel  : 0987-654-3210
address : tokyo nancharakanchara 9-8-7

name : nakazava yuko
tel  : 090-1234-5678
address : kanagawa hogehoge 1234-5678


構造体

10-5.構造体の構造体

    構造体の入れ子を作ることもできます。すなわち、構造体を他の構造体に入れることも 可能というわけです。

    下の例では、「meibo」構造体に、さらに「年齢」と家族データを加えました。 家族データは、住所と電話番号は同じはずなので、年齢と名前だけの構造体です。

#include <stdio.h>

// 構造体の宣言
struct kazoku{
    char name[20];
    int  age;
};

struct meibo{
    char   name[20];
    int    age;
    char   tel[15];
    char   address[40];
    struct kazoku family[3];
};

// 名簿を１件分表示する関数のプロトタイプ宣言
void disp(struct meibo data);

//
// メイン関数
void main(){
    struct meibo classmate[] = {
        { "abe natsuni"  , 19, "012-3456-7890", "hokkaido uncharakanchara 1-2-3", {"taro", 20} },
        { "gotoh mai"    , 19, "0987-654-3210", "tokyo nancharakanchara 9-8-7"  , {{"ichoro", 22}, {"jiro", 23}} },
        { "nakazava yuko", 19, "090-1234-5678", "kanagawa hogehoge 1234-5678"    },
    };

    int i, size = sizeof(classmate) / sizeof(struct meibo);

    // 表示する
    for ( i = 0 ; i < size ; i++ )
        disp(classmate[i]);
}

//
// 名簿を１件分表示する関数
void disp(struct meibo data){
    struct kazoku* p;

    // 空データなら表示しない
    if ( data.name[0] == '\0' )
        return;

    printf("name : %s\n", data.name);
    printf("age  : %d\n", data.age);
    printf("tel  : %s\n", data.tel);
    printf("address : %s\n", data.address);

    // 家族を表示する
    for ( p = data.family ; p->name[0] != '\0' ; p++ )
        printf("family  : name : %s,  age : %d\n", p->name, p->age );

    printf("\n");
}

name : abe natsuni
age  : 19
tel  : 012-3456-7890
address : hokkaido uncharakanchara 1-2-3
family  : name : taro,  age : 20

name : gotoh mai
age  : 19
tel  : 0987-654-3210
address : tokyo nancharakanchara 9-8-7
family  : name : ichoro,  age : 22
family  : name : jiro,  age : 23

name : nakazava yuko
age  : 19
tel  : 090-1234-5678
address : kanagawa hogehoge 1234-5678
    配列の初期化データが足りない場合は、0が入ることを思い出してください。 すなわち、家族データがない場合は、名前なし、年齢0という家族データが 勝手に入ります。


構造体

10-6.構造体と関数

    前に関数の引数に構造体を渡すやり方を簡単に示しました。 ここでは構造体と関数についてもう少し詳しく説明します。 　　既存のデータ型を関数の引数として渡した場合は「値渡し」 と言い、関数の中でその値を変更しても、関数の呼び出し元に戻ってきたときには、 その変更は元に戻ってしまいました。これは変数のコピーが関数で使われているからです。 もし、関数の中で変更する必要がある場合は、「アドレス渡し」 を用いる必要がありました。

    構造体も同じことが言えます。すなわち関数に値渡しで構造体を渡しても、それは コピーになるので、関数ないで変更しても、関数の呼び出し元に戻れば元に戻ってしまいます。

    関数内で変更する必要がある場合は既存のデータ型と同様に、アドレス渡しをさせるか、 関数の戻り値として構造体を返す必要があります。

    構造体のコピーは時間がかかるため、一般には前者のアドレス渡しを用いる場合が 多いようです。

    以下に、名前を大文字に変換する関数の例をアドレス渡しと、値渡しで示します。

#include <stdio.h>

// 構造体の宣言
struct kazoku{
    char name[20];
    int  age;
};

struct meibo{
    char   name[20];
    int    age;
    char   tel[15];
    char   address[40];
    struct kazoku family[3];
};

// 名簿を１件分表示する関数のプロトタイプ宣言
void disp(struct meibo data);

// 名前を大文字に変換する関数のプロトタイプ宣言
void makeupper1(struct meibo* p);
struct meibo makeupper2(struct meibo data);

//
// メイン関数
void main(){
    struct meibo classmate[] = {
        { "abe natsuni"  , 19, "012-3456-7890", "hokkaido uncharakanchara 1-2-3", {"taro", 20} },
        { "gotoh mai"    , 19, "0987-654-3210", "tokyo nancharakanchara 9-8-7"  , {{"ichoro", 22}, {"jiro", 23}} },
        { "nakazava yuko", 19, "090-1234-5678", "kanagawa hogehoge 1234-5678"    },
    };

    int i, size = sizeof(classmate) / sizeof(struct meibo);

    // abe natsuniを大文字にする(アドレス渡し)
    makeupper1(&classmate[0]);

    // gotoh maiを大文字にする(値渡し)
    classmate[1] = makeupper2(classmate[1]);

    // 表示する
    for ( i = 0 ; i < size ; i++ )
        disp(classmate[i]);
}

//
// 名簿を１件分表示する関数
void disp(struct meibo data){
    struct kazoku* p;

    // 空データなら表示しない
    if ( data.name[0] == '\0' )
        return;

    printf("name : %s\n", data.name);
    printf("age  : %d\n", data.age);
    printf("tel  : %s\n", data.tel);
    printf("address : %s\n", data.address);

    // 家族を表示する
    for ( p = data.family ; p->name[0] != '\0' ; p++ )
        printf("family  : name : %s,  age : %d\n", p->name, p->age );

    printf("\n");
}

//
// 名前を大文字に変換する関数(アドレス渡し)
void makeupper1(struct meibo* p){
    char *pname = p->name;
    while( *pname ){
        *pname = toupper(*pname);
        pname++;
    }
}

//
// 名前を大文字に変換する関数(値渡し)
struct meibo makeupper2(struct meibo data){
    char *pname = data.name;
    while( *pname ){
        *pname = toupper(*pname);
        pname++;
    }
    return data;
}

name : ABE NATSUNI
age  : 19
tel  : 012-3456-7890
address : hokkaido uncharakanchara 1-2-3
family  : name : taro,  age : 20

name : GOTOH MAI
age  : 19
tel  : 0987-654-3210
address : tokyo nancharakanchara 9-8-7
family  : name : ichoro,  age : 22
family  : name : jiro,  age : 23

name : nakazava yuko
age  : 19
tel  : 090-1234-5678
address : kanagawa hogehoge 1234-5678


構造体

10-7.練習問題

問題

土星 　　衛星のデータを入れるための構造体を作る。構造体のデータは「衛星の名前」「半径」 「質量」「発見年」「発見者」とする。構造体のデータを代入する関数、構造体のデータを 表示する関数、半径でソートする関数を作る。main関数は、構造体を定義し、初期化する関数を 呼び、ソートし、表示する。

    なお、衛星のデータを代入する関数、メイン関数は用意されているものとする。

解答

#include <stdio.h>
#include <string.h>
#include <conio.h>

//
// 構造体の宣言
    // input here

//
// 構造体のデータを代入
void init(struct satellite data[]){
    strcpy(data[0].name, "パン");
    data[0].radius = 10;
    data[0].weight = -1;
    strcpy(data[0].discover1, "ショーウォルター");
    data[0].discover2 = 1990;

    strcpy(data[1].name, "アトラス");
    data[1].radius = 14;
    data[1].weight = -1;
    strcpy(data[1].discover1, "テリル");
    data[1].discover2 = 1980;

    strcpy(data[2].name, "プロメテウス");
    data[2].radius = 46;
    data[2].weight = 2.70e17;
    strcpy(data[2].discover1, "コリンズ");
    data[2].discover2 = 1980;

    strcpy(data[3].name, "パンドラ");
    data[3].radius = 57;
    data[3].weight = 5.60e17;
    strcpy(data[3].discover1, "コリンズ");
    data[3].discover2 = 1980;

    strcpy(data[4].name, "エピメテウス");
    data[4].radius = 57;
    data[4].weight = 5.60e17;
    strcpy(data[4].discover1, "ウォーカー");
    data[4].discover2 = 1980;

    strcpy(data[5].name, "ヤヌス");
    data[5].radius = 89;
    data[5].weight = 2.01e18;
    strcpy(data[5].discover1, "ドルフュス");
    data[5].discover2 = 1966;

    strcpy(data[6].name, "ミマス");
    data[6].radius = 196;
    data[6].weight = 3.80e19;
    strcpy(data[6].discover1, "ハーシェル");
    data[6].discover2 = 1789;

    strcpy(data[7].name, "エンケラドゥス");
    data[7].radius = 260;
    data[7].weight = 8.40e19;
    strcpy(data[7].discover1, "ハーシェル");
    data[7].discover2 = 1789;

    strcpy(data[8].name, "テシス");
    data[8].radius = 530;
    data[8].weight = 7.55e20;
    strcpy(data[8].discover1, "カッシーニ");
    data[8].discover2 = 1684;

    strcpy(data[9].name, "テレスト");
    data[9].radius = 15;
    data[9].weight = -1;
    strcpy(data[9].discover1, "ライツェマ");
    data[9].discover2 = 1980;

    strcpy(data[10].name, "カリプソ");
    data[10].radius = 13;
    data[10].weight = -1;
    strcpy(data[10].discover1, "パスキュ");
    data[10].discover2 = 1980;

    strcpy(data[10].name, "カリプソ");
    data[10].radius = 13;
    data[10].weight = -1;
    strcpy(data[10].discover1, "パスキュ");
    data[10].discover2 = 1980;

    strcpy(data[11].name, "ディオネ");
    data[11].radius = 560;
    data[11].weight = 1.05e21;
    strcpy(data[11].discover1, "カッシーニ");
    data[11].discover2 = 1684;

    strcpy(data[12].name, "ヘレネ");
    data[12].radius = 116;
    data[12].weight = -1;
    strcpy(data[12].discover1, "ラキュ");
    data[12].discover2 = 1980;

    strcpy(data[13].name, "レア");
    data[13].radius = 765;
    data[13].weight = 2.49e21;
    strcpy(data[13].discover1, "カッシーニ");
    data[13].discover2 = 1672;

    strcpy(data[14].name, "タイタン");
    data[14].radius = 2575;
    data[14].weight = 1.35e23;
    strcpy(data[14].discover1, "ホイヘンス");
    data[14].discover2 = 1655;

    strcpy(data[15].name, "ヘピリオン");
    data[15].radius = 143;
    data[15].weight = 1.77e19;
    strcpy(data[15].discover1, "ボンド");
    data[15].discover2 = 1848;

    strcpy(data[16].name, "イアベトゥス");
    data[16].radius = 730;
    data[16].weight = 1.88e21;
    strcpy(data[16].discover1, "カッシーニ");
    data[16].discover2 = 1671;

    strcpy(data[17].name, "フェーベ");
    data[17].radius = 110;
    data[17].weight = 4.00e18;
    strcpy(data[17].discover1, "ピカリング");
    data[17].discover2 = 1898;

    data[18].name[0] = '\0';
}

//
// ソート関数
void sort(struct satellite data[]){
    // input here
}

//
// 表示
void disp(struct satellite *data){
    // input here
}

//
// メイン関数
void main(){
    struct satellite saturn[19];

    init(saturn);
    sort(saturn);
    disp(saturn);
}


構造体

10-7.練習問題

問題

土星 　　衛星のデータを入れるための構造体を作る。構造体のデータは「衛星の名前」「半径」 「質量」「発見年」「発見者」とする。構造体のデータを代入する関数、構造体のデータを 表示する関数、半径でソートする関数を作る。main関数は、構造体を定義し、初期化する関数を 呼び、ソートし、表示する。

    なお、衛星のデータを代入する関数、メイン関数は用意されているものとする。

解答

#include <stdio.h>
#include <string.h>
#include <conio.h>

//
// 構造体の宣言
struct satellite{
    char   name[20];
    int       radius;
    double weight;
    char   discover1[20];
    int       discover2;
};

//
// 構造体のデータを代入
void init(struct satellite data[]){
    strcpy(data[0].name, "パン");
    data[0].radius = 10;
    data[0].weight = -1;
    strcpy(data[0].discover1, "ショーウォルター");
    data[0].discover2 = 1990;

    strcpy(data[1].name, "アトラス");
    data[1].radius = 14;
    data[1].weight = -1;
    strcpy(data[1].discover1, "テリル");
    data[1].discover2 = 1980;

    strcpy(data[2].name, "プロメテウス");
    data[2].radius = 46;
    data[2].weight = 2.70e17;
    strcpy(data[2].discover1, "コリンズ");
    data[2].discover2 = 1980;

    strcpy(data[3].name, "パンドラ");
    data[3].radius = 57;
    data[3].weight = 5.60e17;
    strcpy(data[3].discover1, "コリンズ");
    data[3].discover2 = 1980;

    strcpy(data[4].name, "エピメテウス");
    data[4].radius = 57;
    data[4].weight = 5.60e17;
    strcpy(data[4].discover1, "ウォーカー");
    data[4].discover2 = 1980;

    strcpy(data[5].name, "ヤヌス");
    data[5].radius = 89;
    data[5].weight = 2.01e18;
    strcpy(data[5].discover1, "ドルフュス");
    data[5].discover2 = 1966;

    strcpy(data[6].name, "ミマス");
    data[6].radius = 196;
    data[6].weight = 3.80e19;
    strcpy(data[6].discover1, "ハーシェル");
    data[6].discover2 = 1789;

    strcpy(data[7].name, "エンケラドゥス");
    data[7].radius = 260;
    data[7].weight = 8.40e19;
    strcpy(data[7].discover1, "ハーシェル");
    data[7].discover2 = 1789;

    strcpy(data[8].name, "テシス");
    data[8].radius = 530;
    data[8].weight = 7.55e20;
    strcpy(data[8].discover1, "カッシーニ");
    data[8].discover2 = 1684;

    strcpy(data[9].name, "テレスト");
    data[9].radius = 15;
    data[9].weight = -1;
    strcpy(data[9].discover1, "ライツェマ");
    data[9].discover2 = 1980;

    strcpy(data[10].name, "カリプソ");
    data[10].radius = 13;
    data[10].weight = -1;
    strcpy(data[10].discover1, "パスキュ");
    data[10].discover2 = 1980;

    strcpy(data[10].name, "カリプソ");
    data[10].radius = 13;
    data[10].weight = -1;
    strcpy(data[10].discover1, "パスキュ");
    data[10].discover2 = 1980;

    strcpy(data[11].name, "ディオネ");
    data[11].radius = 560;
    data[11].weight = 1.05e21;
    strcpy(data[11].discover1, "カッシーニ");
    data[11].discover2 = 1684;

    strcpy(data[12].name, "ヘレネ");
    data[12].radius = 116;
    data[12].weight = -1;
    strcpy(data[12].discover1, "ラキュ");
    data[12].discover2 = 1980;

    strcpy(data[13].name, "レア");
    data[13].radius = 765;
    data[13].weight = 2.49e21;
    strcpy(data[13].discover1, "カッシーニ");
    data[13].discover2 = 1672;

    strcpy(data[14].name, "タイタン");
    data[14].radius = 2575;
    data[14].weight = 1.35e23;
    strcpy(data[14].discover1, "ホイヘンス");
    data[14].discover2 = 1655;

    strcpy(data[15].name, "ヘピリオン");
    data[15].radius = 143;
    data[15].weight = 1.77e19;
    strcpy(data[15].discover1, "ボンド");
    data[15].discover2 = 1848;

    strcpy(data[16].name, "イアベトゥス");
    data[16].radius = 730;
    data[16].weight = 1.88e21;
    strcpy(data[16].discover1, "カッシーニ");
    data[16].discover2 = 1671;

    strcpy(data[17].name, "フェーベ");
    data[17].radius = 110;
    data[17].weight = 4.00e18;
    strcpy(data[17].discover1, "ピカリング");
    data[17].discover2 = 1898;

    data[18].name[0] = '\0';
}

//
// ソート関数
void sort(struct satellite data[]){
    struct satellite tmp;
    int n;
    int i = 0;

    // 要素数を数える
    for ( n = 0 ; data[n].name[0] ; n++ );

    while(i < n){
        int j = i;
        while(j != 0){
            if( data[j-1].radius < data[j].radius )
                j = 0;
            else{
                tmp = data[j-1];
                data[j-1] = data[j];
                data[j] = tmp;
                j--;
            }
        }
        i++;
    }
}

//
// 表示
void disp(struct satellite *data){
    int i;
    for ( i = 1 ; data->name[0] ; data++, i++ ){
        printf("name   : %s\n", data->name);
        printf("radius : %d[km]\n", data->radius);
        printf("weight : %g[kg]\n", data->weight);
        printf("discoverer : %s\n", data->discover1);
        printf("year of discovery : %d\n", data->discover2);
        printf("\n");

        if ( !(i & 3) )
            getch();
    }
}

//
// メイン関数
void main(){
    struct satellite saturn[19];

    init(saturn);
    sort(saturn);
    disp(saturn);
}


共用体

第11章.共用体

11-1.共用体とは

    一般の変数は型を宣言すると、その型しか代入できませんでした。共用体を用いると 異なるデータを代入したりできるようになります。つまりメモリーを色々な型の変数で 共用するということになります。

11-2.共用体の宣言

    共用体の宣言は、構造体の宣言とほぼ同じです。

    union 構造体データ型の名前{
        メンバーの宣言１
        メンバーの宣言２
        メンバーの宣言３
            ：
            ：
    };

例）
    union uni{
        long  a;
        short b;
        char  c;
    };
    上記の例のように宣言した場合、変数a、変数bと変数cは同じメモリー領域に確保されます。 すなわち変数aに値を代入し、その後変数cに値を代入すると、変数aの値は壊れてしまいます。 これをイメージで見ると、以下のようになります。

    構造体と違うことは、初期化ができないと言うことです。 共用体は様々な型が同一メモリー上に定義されているので、初期化子がどの型なのかをコンパイラが 正しく認識できないからです。

    共用体は「構造体を使えば良いじゃないか」と思うような代物ですが、共用体の利点を あげます。

  • 構造体に比べて、メモリーを節約できる
  • ある領域を分割して扱ったり、まとめて扱ったりできる


共用体

11-3.共用体の使用法

    簡単な例を見てみましょう。ただし、下記のプログラムの結果は 実行環境によって異なります。
#include <stdio.h>

union uni{
    long  a;
    short b;
    char  c;
};

struct str{
    long  a;
    short b;
    char  c;
};

void main(){
    union  uni u;
    struct str s;
    int    size_uni = sizeof(u);
    int    size_str = sizeof(s);

    u.a = 0x12345678;
    s.a = 0x12345678;

    printf("u.a = %8x\n",u.a);
    printf("u.b = %4x\n",u.b);
    printf("u.c = %2x\n",u.c);
    printf("sizeof(u) = %d\n\n", size_uni);

    printf("s.a = %8x\n",s.a);
    printf("s.b = %4x\n",s.b);
    printf("s.c = %2x\n",s.c);
    printf("sizeof(s) = %d\n", size_str);
}

intel系CPUなどの場合
u.a = 12345678
u.b = 5678
u.c = 78
sizeof(u) = 4

s.a = 12345678
s.b = 6000    ←値が割り当てられていない領域を表示しようとしたので、おかしい
s.c = 40      ←値が割り当てられていない領域を表示しようとしたので、おかしい
sizeof(s) = 8

68000系CPUなどの場合
u.a = 12345678
u.b = 1234
u.c = 12
sizeof(u) = 4

s.a = 12345678
s.b = 6000    ←値が割り当てられていない領域を表示しようとしたので、おかしい
s.c = 40      ←値が割り当てられていない領域を表示しようとしたので、おかしい
sizeof(s) = 8
    では、次に４バイトの領域(long型)を２バイトの領域(short型)２つとしてアクセスしたり、 １バイトの領域(char型)４つとしてアクセスする例を紹介します。
#include <stdio.h>

struct byte2{
    short s1;
    short s2;
};

struct byte1{
    char c1;
    char c2;
    char c3;
    char c4;
};

union area{
    long         l;
    struct byte2 s;
    struct byte1 c;
};

void main(){
    union area a;
    a.l = 0x12345678;

    printf("%8x\n", a.l);
    printf("%4x %4x\n", a.s.s1, a.s.s2);
    printf("%2x %2x %2x %2x\n", a.c.c1, a.c.c2, a.c.c3, a.c.c4);
}

intel系CPUなどの場合
12345678
5678 1234
78 56 34 12

68000系CPUなどの場合
12345678
1234 5678
12 34 56 78
    しかし、いちいち構造体を作るのも面倒です。そのような場合は配列を使います。
#include <stdio.h>

union area{
    long  l;
    short s[2];
    char  c[4];
};

void main(){
    union area a;
    a.l = 0x12345678;

    printf("%8x\n", a.l);
    printf("%4x %4x\n", a.s[0], a.s[1]);
    printf("%2x %2x %2x %2x\n", a.c[0], a.c[1], a.c[2], a.c[3]);
}

intel系CPUなどの場合
12345678
5678 1234
78 56 34 12

68000系CPUなどの場合
12345678
1234 5678
12 34 56 78


ファイル

第12章.ファイル

12-1.ファイル

    ファイルの入出力には「低水準入出力関数」と 「高水準入出力関数」があります。

    低水準入出力関数はOS依存の関数で、比較的細かい作業が可能になります。ただし OS依存と言うことで、プログラムを他のOSに移植するといった場合、プログラムを変更する 必要が生じます。また、関数自体も低機能な関数しか用意されておらず、今まで使ってきた 「printf」関数のように、文字列をフォーマットして出力することもできません。

    高水準入出力関数は比較的高機能な関数が用意されているので、「printf」関数のように フォーマット付きでファイル出力したりすることも容易にできますし、他のOSに移植する際も ほとんどの場合プログラムの変更はありません。また、バッファリング機能という機能が備わ っており、低水準入出力関数よりもほとんどの場合に、高速な入出力が可能です。

    バッファリング機能とは、簡単に言えばディスクキャッシュのようなものです。 ディスクは、メモリーと違い１バイト毎に管理されているわけではありません。 MS-DOSの2HDフロッピーディスクの場合は、1024バイトが最小単位です。 したがって、1バイト読み込みたい場合でも、1024バイト読み込まなくてはなりません。 バッファリング機能が無い場合、100バイトのファイルを1バイトずつ読み込もうとした場合、 1024×100読み込み作業が行われます。

    バッファリング機能が備わっている場合は、1バイト読み込まれると最小単位である 1024バイトをメモリー上に蓄えます。次の1バイトが読み込もうとした場合、ディスクから ではなく、メモリー上に蓄えられている1024バイトの中から次の1バイトが読み込まれるため、 入出力が早くなるわけです。最近のパソコンには、ディスクキャッシュなどが備わっているため、低水準入出力関数 を使ってもほとんど処理速度には影響しませんが・・・。

    低水準入出力関数については次章で解説します。


ファイル

12-2.ファイル構造体

    ファイル入出力の一連の流れは下記の通りに行います。そして、各処理共通のデータとして 「FILE構造体」が使われます。
  • ファイルを作成するorオープンする(FILE構造体が作られる)
  • ファイルを読むor書く
  • ファイルをクローズする
    FILE構造体は「stdio.h」内で定義されています。例として、WindowsのVisula stadioと UNIX系コンパイラのFILE構造体の定義を紹介します。
Visual C++の場合
#ifndef _FILE_DEFINED
struct _iobuf {
        char *_ptr;	// 次に読まれる位置(ファイルポインタ)
        int   _cnt;	// 現在位置からファイルの最後までのバイト数
        char *_base;	// ファイルの最初の位置(ポインタ)
        int   _flag;	// ファイルアクセスモード
        int   _file;	// フィルディスクリプタ
        int   _charbuf;	// バッファーの位置
        int   _bufsiz;	// バッファーサイズ
        char *_tmpfname;	// 一時的なファイル名
        };
typedef struct _iobuf FILE;
#define _FILE_DEFINED
#endif

Turbo C
typedef struct{
    int             level;	// バッファーの使用量
    unsigned        flags;	// ファイルアクセスモード
    char            fd;	// ファイルディスクリプタ
    unsigned char   hold;	// 文字の保存
    int             bsize;	// バッファーサイズ
    unsigned char   *buffer;	// バッファーの位置
    unsigned char   *curp;	// 次に読まれる位置(ファイルポインタ)
    unsigned        istemp;	// 一時的なファイル
    short           token;	// エラーフラグ
} FILE;

UNIX系コンパイラの場合
extern struct _iobuf{
    int            _cnt;	// 現在位置からファイルの最後までのバイト数
    unsigned char* _ptr;	// 次に読まれる位置(ファイルポインタ)
    unsigned char* _base;	// バッファーの位置
    int            _bufsiz;	// バッファーサイズ
    short          _flag;	// ファイルアクセスモード
    int            _file;	// フィルディスクリプタ
} _iob[];
#define FILE struct _iobuf
    難しそうな定義ですが、実際にFILE構造体の中身を調べたり、書き換えたりすることは よっぽどのことがない限りありません。

    なお、高水準入出力関数を使用するためには「stdio.h」をインクルードする必要があります。


ファイル

12-3.ファイル作成・オープン

    ファイルの作成やオープンするためには「fopen」関数を 使用します。オープンするファイル名やオープンしたファイルをどのように扱うかを 引数に渡します。

関数名 | fopen
書式 | FILE* fopen( const char* filename, const char* mode )
戻り値 NULL＝エラー
NULL≠FILE構造体へのポインタ
機能 第一引数の渡されたファイル名を
第二引数で渡されたモードでオープンする(作成する)

モード(第二引数) | 動作 | ファイルが存在しない場合 | ファイルが存在する場合
"r" 読込モード エラー
"w" | 書込モード | 新しいファイルを作成 | 内容が失われる
"a" | 追加書込 | 新しいファイルを作成 | 既存ファイルの最後に追加
"r+" 読込・書込モード エラー
"w+" | 読込・書込モード | 新しいファイルを作成 | 内容が失われる
"a+" | 読込・追加書込モード | 新しいファイルを作成 | 既存ファイルの最後に追加

モード(第二引数)に
追加する文字 		モード 機能
t テキストモード 読込時：CR,LF('\r','\n')をLF('\n')に変換する
書込時：LF('\n')をCR,LF('\r','\n')に変換する
b バイナリモード テキストモードのような変換を行わない
・UNIXではテキストモード、バイナリモードの区別はなく、すべてバイナリモードで行われる。
・省略した場合は、バイナリモードになる

    例として、バイナリモードで「test.dat」というファイルを読込モードでオープンする 例を示します。

#include <stdio.h>

void main(){
    FILE *fp;
    fp = fopen("test.dat", "rb");

    if ( fp ){
//      読込作業
//      ファイルクローズ
    }
    else{
        printf("ファイルオープンエラー\n");
    }
}


ファイル

12-4.ファイル読み込み

    ファイルを読み込む関数はいくつかありますが、ここでは代表的な「fgetc」関数、 「fgets」関数、「fscanf」関数を紹介します。 これらの関数は今までに紹介した「getc」、「scanf」、 「gets」関数のファイル版です。

関数名 | fgetc
書式 | int fgetc( FILE* fp )
戻り値 EOF：エラーもしくは、ファイル終端
それ以外：読み込んだ文字
機能 ファイルを現在位置から１バイト読み込み、その文字を返す。
ファイルポインタを一つ進める

関数名 | fgets
書式 | char* fgets( char* p, int n, FILE* fp )
戻り値 NULL：エラーもしくは、ファイル終端
それ以外：読み込んだ文字列の格納場所
機能 ファイルを現在位置から「n-1バイト」あるいは、改行(LF='\n')かファイル終端までpに読み込む。
ファイルポインタを読み込んだ分進める。
文字列の最後にはNULLを自動的にふかしてくれる。

関数名 | fscanf
書式 | int fscanf( FILE* fp, const char* format, ... )
戻り値 EOF：エラーもしくは、ファイル終端
それ以外：読み込んだ項目数
機能 第二引数以降はscanf関数と同じです。
読み込んだバイト数だけファイルポインタを進める

    表中の「EOF」は「End Of File」の略で、ファイルの終了を意味する 略語です。「EOF」は「stdio.h」の中で#defineで定義されており、実際には-1を示します。

    例として、「test.txt」というファイルを読込モードでオープンし、それを表示する例を ２例紹介します。

#include <stdio.h>

#define BUFFER_SIZE 200

void main(){
    char  buffer[BUFFER_SIZE];
    FILE *fp;
    fp = fopen("test.txt", "r");

    // ファイルオープン失敗か？
    if ( !fp ){
        printf("ファイルオープンエラー\n");
        return;
    }

    // １行ずつ読み込んで表示する
    while( fgets(buffer, BUFFER_SIZE, fp) ){
        printf("%s", buffer);
    }

    // ファイルクローズ
    fclose(fp);
}

#include <stdio.h>
#include <conio.h>

void main(){
    int  ch;
    FILE *fp;
    fp = fopen("test.txt", "r");

    // ファイルオープン失敗か？
    if ( !fp ){
        printf("ファイルオープンエラー\n");
        return;
    }

    // １文字ずつ読み込んで表示する
    while( (ch = fgetc(fp)) >= 0 ){
        putch(ch);
    }

    // ファイルクローズ
    fclose(fp);
}


ファイル

    次に、ブロック読み出しについて説明します。ブロック読み出しとは、その名の通り、 固まりごと読み出すことを言います。例えば配列と一緒に用いて、int型のデータ 10個分を一度に読み出すとか、構造体と一緒に読み出すと行った具合です。

関数名 | fread
書式 | size_t fread( void* buffer, size_t size, size_t count, FILE* fp )
戻り値 | EOF：エラー それ以外：実際に読み込んだ項目数
機能 | ファイルを現在位置から、１項目sizeバイトのデータをcount個読み込み、bufferに格納する。 戻り値がcountよりも小さい場合は、エラーかファイル終端に到達したことになります。
引数の「size_t」は「unsigned int」と同じで、主にサイズ(単位は、個、バイトなど様々)を 意味します。

    例として、「class.dat」というファイルを読込モードでオープンし、それを表示する例を ２例紹介します。class.datにはあらかじめ名簿データが入っている物とします。 名簿データはここからダウンロードするか、 ブロック書込のサンプルプログラムで作ってください。

#include <stdio.h>

// 構造体の宣言
struct meibo{
    char name[20];
    char tel[15];
    char address[40];
};

//
// 表示関数
void dispall(struct meibo *p){
    // 名前がない場合は終了する
    while( p->name[0] != '\0' ){
        printf("name : %s\n", p->name);
        printf("tel  : %s\n", p->tel);
        printf("address : %s\n\n", p->address);

        p++;
    }
}

void main(){
    struct meibo classmate[100];
    size_t n;
    FILE   *fp;

    fp = fopen("class.dat", "r");

    // ファイルオープン失敗か？
    if ( !fp ){
        printf("ファイルオープンエラー\n");
        return;
    }

    // １名ずつ読み込んで表示する
    n = fread( classmate, sizeof(struct meibo), sizeof(classmate)/sizeof(struct meibo)-1, fp );
    dispall(classmate);

    // ファイルクローズ
    fclose(fp);
}


ファイル

12-5.ファイル書き込み

    ファイルを書き込む関数もいくつかありますが、ここでは代表的な「fputc」関数、 「fputs」関数、「fprintf」関数を紹介します。 これらの関数は今までに紹介した「getc」、「scanf」、 「puts」関数のファイル版です。

関数名 | fputc
書式 | int fputc(int ch, FILE* fp )
戻り値 EOF：エラーもしくは、ファイル終端
それ以外：書き込んだ文字
機能 ファイルの現在位置から１バイト書き込み、その文字を返す。
ファイルポインタを一つ進める

関数名 | fputs
書式 | int fputs( char* p, FILE* fp )
戻り値 EOF：エラー
それ以外：書き込んだ最後の文字
機能 ファイルを現在位置へ、pからNULL文字までファイルに書き込む。
ファイルポインタを書き込んだ分進める。
文字列の最後にはNULLを自動的にふかしてくれる。

関数名 | fprintf
書式 | int fprintf( FILE* fp, const char* format, ... )
戻り値 負の値：エラー
それ以外：書き込んだ文字数
機能 第二引数以降はprintf関数と同じです。
書き込んだバイト数だけファイルポインタを進める

    表中の「EOF」は「End Of File」の略で、ファイルの終了を意味する 略語です。「EOF」は「stdio.h」の中で#defineで定義されており、実際には-1を示します。

    例として、「test.txt」というファイルを追加書き込みモードでオープンし、 「ABCDEFG」を追加書き込みする例を２例紹介します。

#include <stdio.h>

void main(){
    char *buffer = "ABCDEFG";
    FILE *fp;
    fp = fopen("test.txt", "a");

    // ファイルオープン失敗か？
    if ( !fp ){
        printf("ファイルオープンエラー\n");
        return;
    }

    // 文字を書き込む
    if ( EOF == fputs(buffer, fp) )
        printf("ファイル書き込みエラー\n");

    // ファイルクローズ
    fclose(fp);
}

#include <stdio.h>
void main(){
    char *buffer = "ABCDEFG";
    FILE *fp;
    fp = fopen("test.txt", "a");

    // ファイルオープン失敗か？
    if ( !fp ){
        printf("ファイルオープンエラー\n");
        return;
    }

    // 文字を書き込む
    if ( EOF == fprintf(fp, buffer) )
        printf("ファイル書き込みエラー\n");

    // ファイルクローズ
    fclose(fp);
}


ファイル

    次にブロック書き込みの関数を紹介します。ブロック書き込みは、 ブロック読み込みのところで説明したように、配列や構造体などをまとめて読み書きする ことを言います。

関数名 | fwrite
書式 | size_t fwrite( void* buffer, size_t size, size_t count, FILE* fp )
戻り値 | 戻り値<count：エラー 戻り値=count：正常実行
機能 | bufferに格納されているsizeバイトのデータをcount個書き込み、ファイルポインタを進めます。
引数の「size_t」は「unsigned int」と同じで、主にサイズ(単位は、個、バイトなど様々)を 意味します。

    例として、「class.dat」というファイルを書き込みモードでオープンし、 名簿データを書き込みます。これを読むプログラムはこちらです。

#include <stdio.h>

// 構造体の宣言
struct meibo{
    char name[20];
    char tel[15];
    char address[40];
};

//
// 表示関数
void dispall(struct meibo *p){
    // 名前がない場合は終了する
    while( p->name[0] != '\0' ){
        printf("name : %s\n", p->name);
        printf("tel  : %s\n", p->tel);
        printf("address : %s\n\n", p->address);

        p++;
    }
}

void main(){
    struct meibo classmate[] = {
        { "abe natsuni"  , "012-3456-7890", "hokkaido uncharakanchara 1-2-3" },
        { "gotoh mai"    , "0987-654-3210", "tokyo nancharakanchara 9-8-7"   },
        { "nakazava yuko", "090-1234-5678", "kanagawa hogehoge 1234-5678"    },
        { "" },
    };

    size_t n;
    FILE   *fp;

    fp = fopen("class.dat", "w");

    // ファイルオープン失敗か？
    if ( !fp ){
        printf("ファイルオープンエラー\n");
        return;
    }

    // １名ずつ読み込んで表示する
    n = fwrite( classmate, sizeof(struct meibo), sizeof(classmate)/sizeof(struct meibo), fp );
    dispall(classmate);

    // ファイルクローズ
    fclose(fp);
}


ファイル

12-6.ファイルクローズ

    ファイルをオープンしたら、クローズしなければなりません。一度に開くことができる ファイルの数は限られていますし、高水準入出力関数を用いたファイル操作では、 fprintfなどの書き込み操作を行った時点ではディスクに書き込まず、バッファーに書き込まれます。 クローズ処理(実際にはフラッシュ処理)を行ったときにはじめてディスクに書き込まれるからです。

関数名 | fclose
書式 | int fclose( FILE* fp )
戻り値 EOF：エラー
0：正常終了
機能 | ファイルをフラッシュし、クローズする

関数名 | fcloseall
書式 | int fcloseall()
戻り値 EOF：エラー
0：正常終了
機能 標準入力(stdin)、標準出力(stdout)、標準エラー出力(stderr)、(MS-DOS系OSでは、 標準補助入出力 (stdaux) と標準プリンタ出力 (stdprn) )を除くファイルをフラッシュしてクローズする。
一時ファイルは削除されます。

関数名 | fflush
書式 | int fflush( FILE* fp )
戻り値 EOF：エラー
0：成功かフラッシュする必要がない
機能 書込モード：実際にディスクに書き込む
読込モード：バッファーをクリアする
FILE構造体のポインタがNULLの場合：すべてのファイルをフラッシュする

    表中の「EOF」は「End Of File」の略で、ファイルの終了を意味する 略語です。「EOF」は「stdio.h」の中で#defineで定義されており、実際には-1を示します。


ファイル

12-7.ファイルエラー

    今までに紹介した関数では成功したか失敗したかのどちらかしかわからなかった。 読込中(書込中)にディスクが抜かれたり、書込中にディスクがいっぱいになったとかいう エラーなのか、読込中にファイルの終端まで読み出してしまったのか、せめてこれくらいの区別を しないと、処理が正常に終了したのかどうかわからない。このように、今までに紹介した関数が 失敗したときに、エラーなのかどうかを調べる関数を紹介します。

関数名 | feof
書式 | int feof( FILE* fp )
戻り値 0：ファイル終端ではない
0以外：ファイル終端
機能 | ファイルの終端まで到達したかどうかを調べる

関数名 | ferror
書式 | int ferror( FILE* fp )
戻り値 0：エラーなし
0以外：エラーが発生している
機能 | 直前のファイル操作でエラーが発生しているかどうかを調べる

関数名 | fclearerr
書式 | void fclearerr( FILE* fp )
戻り値 | なし
機能 | エラーフラグをクリアする


ファイル

12-8.ランダムアクセス

    これまでに紹介した読込関数、書込関数は現在位置を基点にしていました。 しかし、ファイルポインタを進めたり、戻したりすることも頻繁にあります。 このようにファイルポインタを動的に移動して、ファイルの任意の位置を読み書き する使い方を「ランダムアクセス」といいます。これに対して、 ファイルの最初から最後まで、全て順に読み書きする使い方を「シーケンシャル アクセス」と言います

    ここでは、ランダムアクセスに必要なファイルポインタの移動をする 関数を紹介します。

関数名 | ftell
書式 | long ftell( FILE* fp )
戻り値 -1：エラー
-1以外：現在の位置
機能 ファイルポインタの位置を返す
(戻り値が0の時はファイルの最初を示している)

関数名 | fseek
書式 | int fseek( FILE* fp, long offset, int origin )
戻り値 0：エラーなし
0以外：エラーが発生している
機能 ファイルポインタをoriginからoffsetバイトの新しい位置に移動します。 以後の処理は移動したファイルポインタを基点に行われます。

また、originは以下の値を 指定します。
  SEEK_SET・・・ファイルの先頭を基点にする
  SEEK_CUR・・・現在のファイルポインタを基点にする
  SEEK_END・・・ファイルの終端を基点にする

stdio.hに#defineされていますが、コンパイラによっては定義されていない場合があります。 その場合は以下の値を使います。
  0・・・ファイルの先頭を基点にする
  1・・・現在のファイルポインタを基点にする
  2・・・ファイルの終端を基点にする

関数名 | frewind
書式 | void frewind( FILE* fp )
戻り値 0：正常実行
0以外：エラー
機能 | ファイルポインタをファイルの先頭に移し、エラーフラグをクリアします。

    昔のコンパイラを使用している場合で、ファイルを読み書き両用モードで開いている時は 注意が必要です。読み込みを行った後に書き込みを行う場合やその逆を行う場合は、 必ずfseekを呼ばなければなりません。もしこれを忘れると、場合によってはバッファー内と 実際にディスクに描き込まれたデータに矛盾が生じ、正確に書き込まれない場合や、嘘の データを読み込む場合があります。

    ファイルをテキストモードでオープンしている場合のfseek関数も注意が必要です。 テキストモードの場合、CR,LF('\r''\n')をLF('\n')に変換するため、これを１バイトと 計算するか２バイトと計算するか、コンパイラによって異なるからです。また、 fseek関数の引数「offset」は0でないと、誤動作する場合もあるようです。


ファイル

12-9.標準入出力

    UNIXはすべてのデバイスをファイルと同様に扱うという仕様があり、これを受け継いでMS-DOSも 同様の仕様になっています。例えば、ディスプレイやキーボードもファイルの一種と考えることが できます。

    ディスプレイやキーボードは常にオープンされていて、いつでも入出力が可能な状態に なっています。これらのファイルポインタ(FILE構造体)は定数になっており、ディスプレイは 「stdout」、「stderr」という変数に代入されています。 キーボードは「stdin」という変数に代入されています。 これらは「stdio.h」内で定義されています。

    したがって、ディスプレイに何か文字を表示したい場合は下記のような書き方でも 出力できます。

#include <stdio.h>

// キーボードから入力した文字をディスプレイに表示させる例
void main(){
    char buffer[100];

    // キーボードから入力
    gets(buffer);

    // ディスプレイに表示
    puts(buffer);
}

#include <stdio.h>

// キーボードから入力した文字をディスプレイに表示させる例
void main(){
    char buffer[100];

    // キーボードから入力
    fgets(buffer, sizeof(buffer), stdin);

    // ディスプレイに表示
    fputs(buffer, stdout);
}

abcdefg
abcdefg
    標準出力(stdout)、標準エラー出力(stderr)、標準入力(stdin)は、常にオープンされて いる状態ですので、ファイルオープン(fopen)や、 ファイルクローズ(fclose)する必要はありません。

    ところで、ディスプレイは「stdout」と「stderr」と書きました。ではこの２つはどう違うのでしょう？ 「stdout」は標準出力と言い、普通の表示です。「stderr」は標準エラー出力と言い、 エラー表示などに使用します。

    例えば、MS-DOSの「type」コマンドやUNIXの「cat」コマンドはファイルの中身を表示する コマンドですが、ファイルの中身の表示は標準出力(stdout)に出力されます。したがって リダイレクション機能やパイプ機能を使うと、ディスプレイに表示されるはずのファイルの中身が ファイルなどに出力されます。

C:\>TYPE CONFIG.SYS
device=C:\WINDOWS\himem.sys
device=C:\WINDOWS\EMM386.EXE RAM
devicehigh=C:\WINDOWS\kkcfunc.sys
devicehigh=C:\WINDOWS\hrtimer.sys

dos=high,umb

C:\>TYPE CONFIG.SYS > CONFIG.ORG     ←リダイレクション機能

C:\>TYPE CONFIG.ORG                  ←CONFIG.ORGというファイルができた
device=C:\WINDOWS\himem.sys
device=C:\WINDOWS\EMM386.EXE RAM
devicehigh=C:\WINDOWS\kkcfunc.sys
devicehigh=C:\WINDOWS\hrtimer.sys

dos=high,umb

C:\>
    次に存在しないファイル名を指定した場合は「ファイルが見つかりません.」という エラーメッセージが表示されます。これは標準出力ではなく、標準エラー出力(stderr)です。 標準エラー出力では、リダイレクション機能やパイプ機能は使えません。
C:\>TYPE CONFI.SYS
ファイルが見つかりません. -CONFI.SYS

C:\>TYPE CONFI.SYS > CONFIG.ORG
ファイルが見つかりません. -CONFI.SYS     ←リダイレクション機能を用いてもエラーはディスプレイに表示される

C:\>TYPE CONFIG.ORG                    ←ファイルはできているが、中身はから

C:\>


ファイル

トップ-> C言語入門：ファイル-> 12-10. 練習問題
12-10. 練習問題

問題

「meibo」という構造体を定義する。この構造体は「name」というchar型20個分の配列と、 int型の「age」という２つのメンバー変数を持っている。main関数の引数にファイル名を 指定し、このファイルにキーボードから入力したmeiboを追加書き出しする。最後に 確認のためにファイル内すべてのデータを表示する。ただし、ファイルがない場合は 新規に作成する。

← 前ページへ　:　 解答へ　:　 次章へ →


ファイル

12-10. 練習問題

問題

「meibo」という構造体を定義する。この構造体は「name」というchar型20個分の配列と、 int型の「age」という２つのメンバー変数を持っている。main関数の引数にファイル名を 指定し、このファイルにキーボードから入力したmeiboを追加書き出しする。最後に 確認のためにファイル内すべてのデータを表示する。ただし、ファイルがない場合は 新規に作成する。

解答

#include <stdio.h>
#include <stdlib.h>

// 構造体meiboの定義
struct meibo{
    char name[20];
    int  age;
};

void dispall(FILE *fp);               // ファイルの内容を表示する関数のプロトタイプ宣言
int  inputmeibo(struct meibo* data);  // キーボードからmeiboを１件入力する関数のプロトタイプ宣言

void main(int argc, char *argv[]){
    FILE *fp;
    struct meibo work;

    // 引数のチェック
    if ( argc < 2 ){
        printf("ファイル名を指定してください\n");
        return;
    }

    // ファイルオープン
    if ( !(fp = fopen(argv[1], "a+" ) ) ){
        printf("ファイルを作成できません\n");
        return;
    }

    // ファイルポインタをファイルの最後に移動する
    fseek(fp, 0, 2);

    // キーボードから入力
    while( inputmeibo(&work) ){
        // ファイル書き込み
        if ( fwrite(&work, sizeof(struct meibo), 1, fp) != 1 ){
            printf("ファイル書き込みに失敗しました\n");
            break;
        }
    }

    // ファイルポインタをファイルの最初に移動する
    fseek(fp, 0, 0);

    // ファイルの内容を表示する
    dispall(fp);
}

// ファイルの内容を表示する
void dispall(FILE *fp){
    struct meibo work;

    // ファイルから内容を１項目分読む
    while( fread(&work, sizeof(struct meibo), 1, fp) == 1){
        printf("名前：%s\n",   work.name);
        printf("年齢：%d\n\n", work.age );
    }
}

// キーボードから入力する
// 何も入力されなかったら0、何か入力されたら1を返す
int inputmeibo(struct meibo* data){
    char agetmp[20];

    // 名前の入力
    printf("名前--->");
    gets(data->name);

    if ( data->name[0] == '\0' )
        return 0;

    printf("年齢--->");
    gets(agetmp);
    data->age = atoi(agetmp);

    printf("\n");
    return 1;
}


低水準入出力関数

第13章.低水準入出力関数

13-1.高水準入出力関数との違い

    低水準入出力関数とは、前述したように、OS依存の関数で、比較的細かい作業が可能に なります。ただしOS依存と言うことで、プログラムを他のOSに移植するといった場合、 プログラムを変更する必要が生じます。また、関数自体も低機能な関数しか用意されておらず、 今まで使ってきた「printf」関数のように、文字列をフォーマットして出力することもできません。

    高水準入出力では、ファイルをディスクなどから読み込み、それを一度OS等が管理する メモリーに置きます。そして高水準入力関数(fscanfなど)を 呼び出すと、プログラマーが指定したメモリー領域にコピーされます。しかし、低水準 入力関数では、ディスクなどから直接プログラマーが指定したメモリー領域に書き込まれます ので、ある決まったバイト数を１度だけ読んだり、書いたりする場合には、高水準入出力関数 よりも高速にアクセスができます。

    高水準入出力関数では、ファイルポインタ(FILE構造体のポインタ) を使用してきましたが、低水準入出力関数では ファイルディスクリプタというものを使用します。ファイルディスクリプタ は、「int」型の数値です。

    なお、低水準入出力関数を使用するためには、<io.h>をインクルードする必要があります。

    では次ページから低水準入出力関数の使い方を説明していきます。


低水準入出力関数

13-2.ファイル作成

    高水準入出力関数でファイルを作成するときは、fopen関数 を用いました。低水準入出力関数では、以下のcreat関数を使用します。
関数名 | creat
書式 | int creat( const char* filename, int pmode );
戻り値 -1:エラー
その他:ファイルディスクリプタ
機能 第一引数の渡されたファイル名を
第二引数で渡されたモードでオープンする(作成する)
モード(第二引数) | 動作
S_IREAD | 読み込み専用モード
S_IWRITE | 読み込み・書き込みモード
「S_IREAD」や「S_IWRITE」は「sys\stat.h」で定義されていますので、 このファイルをインクルードする必要があります。
エラーコード | 意味
EACCES | ファイル名に既存の読み出し専用ファイルを指定したか、またはファイルではなくディレクトリを指定しています。
EMFILE | 開いているファイルが多すぎます
「stdlib.h」をインクルードすると、自動的にerrnoというグローバル変数が作られます。 エラーが起きた場合は、この変数にエラーコードが自動的に代入されます。ただし、これら エラーコードは「errno.h」で定義されています。
    以下にサンプルを示します。
#include <io.h>
#include <sys\stat.h>
#include <sys\types.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>

void main(){
    // ファイルディスクリプタ
    int   fd;

    // ファイル作成
    fd = creat( "abc.txt", S_IWRITE );

    if ( fd == -1 ){
        printf("ファイル作成エラー：");
        switch ( errno ){
        case EACCES :
            printf("ファイル名に既存の読み出し専用ファイルを指定したか、");
            printf("またはファイルではなくディレクトリを指定しています。 \n");
            break;
        case EMFILE :
            printf("開いているファイルが多すぎます\n");
            break;
        }
        return;
    }

    // ファイルクローズ
    close( fd );
}


低水準入出力関数

13-3.ファイルオープン

    前ページではファイルの作成方法を説明しました。ここでは ファイルのオープンですが、ファイルオープン関数でもファイルの作成を行うことが できます。

関数名 | open
書式 | int open( const char* filename, int oflag[, int pmode] );
戻り値 -1:エラー
その他:ファイルディスクリプタ
機能 第一引数の渡されたファイル名を
第二引数で渡されたモードでオープンする(作成する)
ただし、ファイル作成モードでは第三引数にファイル属性を指定する
モード(第二引数) | 動作
O_RDONLY | 読み込み専用モード
O_WRONLY | 書き込み専用モード
O_RDWR | 読み込み・書き込みモード
O_CREAT | ファイルの新規作成
O_TRUNC | 既存ファイルをオープンし、中身を空にする
O_EXCL | ファイルが存在すれば、新規作成しない
O_APPEND | 追加書込モード
O_TEXT | テキストモード
O_BINARY | バイナリーモード
上表のモードは「fcntl.h」で定義されているので、これをインクルードする必要がある。
モード(第三引数) | 動作
S_IREAD | 読み込み専用モード
S_IWRITE | 読み込み・書き込みモード
・第三引数は、第二引数が「O_CREAT」の時のみ指定する。
・第二引数の「S_IREAD」や「S_IWRITE」は「sys\stat.h」で定義されていますので、 このファイルをインクルードする必要があります。
エラーコード | 意味
EACCES | ファイル名に既存の読み出し専用ファイルを指定したか、またはファイルではなくディレクトリを指定しています。
EMFILE | 開いているファイルが多すぎます
「stdlib.h」をインクルードすると、自動的にerrnoというグローバル変数が作られます。 エラーが起きた場合は、この変数にエラーコードが自動的に代入されます。ただし、これら エラーコードは「errno.h」で定義されています。
    第二引数について補足します。「テキストモードでファイル読み込み・書き込みモード でオープンする。ファイルがなければ新規作成する」といった場合は、 「O_TEXT | O_RDWR | O_CREAT」というようにor演算子「|」を用いることで、 モードを複数指定することもできる。


低水準入出力関数

13-4.ファイル読み込み

    高水準入出力関数のファイル読み込みは、 「ファイルから１行読む」ことが可能など、高機能な操作が可能でした。

    しかし、低水準入出力関数のファイル読み込み関数は、指定したサイズ分 読み込むことしかできません。

関数名 | read
書式 | int read(int fd, void* buffer, unsigned int count );
戻り値 -1:エラー
0:ファイルの終端で呼ばれた
その他:実際に読み込んだサイズ(バイト)
機能 | オープンされているファイルディスクリプタ(fd)をcountバイト読み込み、bufferに格納する
エラーコード 意味
EBADF ・fdが無効
・ファイルが読み込み用に開かれていない
・ファイルがロックされている
「stdlib.h」をインクルードすると、自動的にerrnoというグローバル変数が作られます。 エラーが起きた場合は、この変数にエラーコードが自動的に代入されます。ただし、これら エラーコードは「errno.h」で定義されています。
    一般に引数countと、関数の戻り値は一致するはずですが、次の場合一致しません。
  • エラーが起きた場合
  • ファイルの終端に達した場合
  • テキストモードで開いていて、改行コードを読んだ場合
    テキストモードで開いている場合、ファイル中に改行コード(CR, LF="\r\n")があると、 読み出し時にLF(='\n')に変換されます。したがって、countで２と指定しても、 実際の戻り値は１になります。

    次ページでは、例としてMS-DOSのTYPEコマンドを 作ってみます。


低水準入出力関数

例題

    例としてMS-DOSのTYPEコマンドを作ってみます。

    TYPEコマンドとはテキストファイルの内容を表示するもので、UNIXのcatコマンドと ほぼ同じ物です。パラメータ(引数)を１つだけとり、パラメータがない場合は 「パラメータが足りません.」と、パラメータが多い場合は「パラメータが多すぎます.」と 表示します。また、パラメータに「/?」を入力すると簡単な使用方法を表示します。

    パラメータを１つしていすると、そのファイルの中身を表示します。

#include <io.h>
#include <sys\stat.h>
#include <sys\types.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <conio.h>

void main(int argc, char *argv[]){
    int   fd;                    // ファイルディスクリプタ
    int   flag;                  // read関数の戻り値
    char  ch;                    // 読み込んだ文字

    // パラメータが少ない場合
    if ( argc < 2 ){
        printf("パラメータが足りません.\n\n");
        return;
    }

    // パラメータが多い場合
    if ( argc > 2 ){
        printf("パラメータが多すぎます.\n\n");
        return;
    }

    // パラメータは「/?」か?
    if ( strcmp(argv[1], "/?" ) == 0 ){
        printf("テキスト ファイルの内容を表示します.\n\n");
        printf("MYTYPE [ドライブ:][パス]ファイル名\n\n");
        return;
    }

    // ファイルオープン
    fd = open(argv[1], O_RDONLY | O_TEXT );

    if ( fd == -1 ){
        switch ( errno ){
        case EACCES :
            printf("アクセスは拒否されました");
            break;
        case EMFILE :
            printf("開いているファイルが多すぎます");
            break;
        default :
            printf("ファイルが見つかりません.");
        }
        printf(" - %s\n\n", argv[1]);
        return;
    }

    // 1バイト読む
    flag = read(fd, &ch, 1);
    while( flag == 1 ){
        // 読み込んだ文字を表示
        putch( (int)ch );

        // 1バイト読む
        flag = read(fd, &ch, 1);
    }

    // ファイルクローズ
    close( fd );
}

C:\>MYTYPE C:\CONFIG.SYS
device=C:\WINDOWS\himem.sys
device=C:\WINDOWS\EMM386.EXE RAM
devicehigh=C:\WINDOWS\kkcfunc.sys
devicehigh=C:\WINDOWS\hrtimer.sys

dos=high,umb
lastdrive=z

C:\>


低水準入出力関数

13-5.ファイル書き込み

    高水準入出力関数のファイル書き込みは、 「ファイルへ１行書く」ことが可能など、高機能な操作が可能でした。

    しかし、低水準入出力関数のファイル書き込み関数は、指定したサイズ分 書き込みことしかできません。

関数名 | write
書式 | int write(int fd, void* buffer, unsigned int count );
戻り値 -1:エラー(ディスク容量が足りないなど)
その他:実際に書き込んだサイズ(バイト)
機能 | bufferに格納されている内容をオープンされているファイルディスクリプタ(fd)へcountバイト書き込みます
エラーコード | 意味
EBADF ・fdが無効
・ファイルが書き込み用に開かれていない
ENOSPC | ・ディスク容量が足りない
「stdlib.h」をインクルードすると、自動的にerrnoというグローバル変数が作られます。 エラーが起きた場合は、この変数にエラーコードが自動的に代入されます。ただし、これら エラーコードは「errno.h」で定義されています。
    通常現在のファイル位置から書き込まれます。ファイルが追加モードで開かれている場合は、 ファイルの最終位置から書き込みます。書き込み処理後、ファイル ポインタは実際に 書き込まれたバイト数だけ進められます。

    次ページでは、例としてMS-DOSのCOPYコマンドを 作ってみます。


低水準入出力関数

例題

    例としてMS-DOSのCOPYコマンドもどきを作ってみます。

    COPYコマンドとはファイルをコピーするものです。MS-DOSのCOPYコマンドでは、 複数のファイルをつなげたり、コピー先はディレクトリの指定だけで良いなど、 これから作るコマンドよりも高機能です。

    ここで作るCOPYコマンドは、パラメータを２つとり、１つ目にはコピー元の ファイルを指定し、２つ目のパラメータにはコピー先のファイル名を指定するようにします。

#include <io.h>
#include <sys\stat.h>
#include <sys\types.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>

void main(int argc, char *argv[]){
    int   fdIn;                  // 読み込み用ファイルディスクリプタ
    int   fdOut;                 // 書き込み用ファイルディスクリプタ
    int   flag;                  // read関数の戻り値
    char  ch;                    // 読み込んだ文字

    // パラメータが少ない場合
    if ( argc < 3 ){
        printf("パラメータが足りません.\n\n");
        return;
    }

    // パラメータが多い場合
    if ( argc > 3 ){
        printf("パラメータが多すぎます.\n\n");
        return;
    }

    // パラメータは「/?」か?
    if ( strcmp(argv[1], "/?" ) == 0 ){
        printf("ファイルをコピーします.\n\n");
        printf("MYCOPY [ドライブ:][パス]ファイル名(コピー元) [ドライブ:][パス]ファイル名(コピー先)\n\n");
        return;
    }

    // ファイルオープン
    fdIn  = open(argv[1], O_RDONLY | O_BINARY  );

    if ( fdIn == -1 ){
        switch ( errno ){
        case EACCES :
            printf("アクセスは拒否されました");
            break;
        case EMFILE :
            printf("開いているファイルが多すぎます");
            break;
        default :
            printf("ファイルが見つかりません.");
        }
        printf(" - %s\n\n", argv[1]);
        return;
    }

    // ファイルオープン
    fdOut = open(argv[2], O_RDWR   | O_CREAT );
    if ( fdOut == -1 ){
        switch ( errno ){
        case EACCES :
            printf("アクセスは拒否されました");
            break;
        case EMFILE :
            printf("開いているファイルが多すぎます");
            break;
        default :
            printf("ファイルが作成できません.");
        }
        printf(" - %s\n\n", argv[2]);
        close( fdIn );
        return;
    }

    // 1バイト読む
    flag = read(fdIn, &ch, 1);
    while( flag == 1 ){
        // 読み込んだ文字を書き込む
        flag = write( fdOut, &ch, 1 );

        // 前の１バイトが正常に読み書き成功なら次の１バイトを読む
        if ( flag == 1 )
            // 1バイト読む
            flag = read(fdIn, &ch, 1);
    }

    // ファイルクローズ
    close( fdIn  );
    close( fdOut );
}


低水準入出力関数

13-6.ファイルクローズ

    高水準入出力関数でも低水準入出力関数でも開いたファイルはクローズしなければ なりません。
関数名 | close
書式 | int close(int fd );
戻り値 -1:エラー(ファイルディスクリプタが無効)
0:クローズ成功
機能 | 開いているファイルディスクリプタをクローズします
エラーコード 意味
EBADF fdが無効
「stdlib.h」をインクルードすると、自動的にerrnoというグローバル変数が作られます。 エラーが起きた場合は、この変数にエラーコードが自動的に代入されます。ただし、これら エラーコードは「errno.h」で定義されています。
    今までの例題でも出てきていますので、特に説明はいらないでしょう。


低水準入出力関数

13-7.標準入出力

    高水準入出力関数では、「stdin」「stdout」 「stderr」は標準入出力用の特別な ファイル構造体であることは述べました。

    低水準入出力関数でも標準入出力用の特別なファイルディスクリプタが用意されています。

ファイルポインタ
(高水準入出力関数) 		ファイルディスクリプタ
(低水準入出力関数) 		備考
標準入力 | stdin | 0 | 通常キーボード
標準出力 | stdout | 1 | 通常ディスプレイ
標準エラー出力 | stderr | 2 | 通常ディスプレイ
    低水準入出力関数でも、これらのファイルディスクリプタはオープンやクローズをする必要は ありません。


低水準入出力関数

13-8.ランダムアクセス

    ファイルの入出力する位置を設定します。例えば、ファイルの最初に戻りたい場合や ファイルの最後に進めたい場合などにはlseek関数を用います。
関数名 | lseek
書式 | long lseek(int fd, long offset, int origin );
戻り値 -1:エラー
その他:ファイル先頭からのバイト数
機能 | originの位置からoffsetバイト目に移動します。
モード(第三引数) | 動作
L_SET | ファイルの先頭からoffsetバイト目に移動 (UNIXのみ)
L_XTND | ファイルの終端からoffsetバイト目に移動 (UNIXのみ)
L_INCR | 何もしない(現在位置を知りたい場合) (UNIXのみ)
SEEK_SET | ファイルの先頭からoffsetバイト目に移動
SEEK_CUR | 現在位置からoffsetバイト目に移動
SEEK_END | ファイルの終端からoffsetバイト目に移動
上表のモードは「stdio.h」で定義されているので、これをインクルードする必要がある。
エラーコード | 意味
EBADF | fdが無効
EINVAL | originの値が無効か、またはoffsetの値がファイルの先頭より前
「stdlib.h」をインクルードすると、自動的にerrnoというグローバル変数が作られます。 エラーが起きた場合は、この変数にエラーコードが自動的に代入されます。ただし、これら エラーコードは「errno.h」で定義されています。


低水準入出力関数

13-9.ファイルポインタとファイルディスクリプタ

    高水準入出力は低水準入出力を拡張したものです。低水準入出力関数を用いてファイルを オープンした場合でも、高水準入出力関数を用いて入出力を行うこともできます。そのためには ファイルディスクリプタをFILE構造体のポインタ(ファイルポインタ)に拡張しなければなりません。 それにはfdopen関数を用います。
関数名 | fdopen
書式 | FILE* fdopen(int fd, const char* mode );
戻り値 NULL(0):エラー
その他:FILE構造体のポインタ(ファイルポインタ)
機能 | ファイルディスクリプタfdを拡張してバッファー入出力機能(ストリーム)を 追加し、以降高水準入出力関数を用いることができるようにします。
モード(第二引数) | 動作 | ファイルが存在しない場合 | ファイルが存在する場合
"r" 読込モード エラー
"w" | 書込モード | 新しいファイルを作成 | 内容が失われる
"a" | 追加書込 | 新しいファイルを作成 | 既存ファイルの最後に追加
"r+" 読込・書込モード エラー
"w+" | 読込・書込モード | 新しいファイルを作成 | 内容が失われる
"a+" | 読込・追加書込モード | 新しいファイルを作成 | 既存ファイルの最後に追加

    なお、FILE構造体にはファイルディスクリプタが含まれていますので、高水準入出力で オープンしたファイルを低水準入出力関数を用いて読み込みや書込を行うこともできます。 しかし、このような使い方をすると、バッファーとの矛盾が生じ、結果的に正しく 読み書きできない場合がありますので、高水準入出力関数である fopenでオープンしたファイルは、低水準入出力関数を用いて読み書きしないでください。


低水準入出力関数

13-10.標準入出力

問題

    MS-DOSのDUMPコマンドを作れ。DUMPコマンドとは、コマンドラインからパラメータを １つとり、パラメータで指定されたファイルを16進数で表示するプログラムである。

    下図のように「アドレスフィールド」「16進数フィールド」「ASCIIフィールド」から 構成されており、ASCIIフィールドは、対応する文字を表示する。ただし、0x20未満と 0x7F以上は'.'を表示する。

00000000  23 69 6E 63 6C 75 64 65 20 3C 69 6F 2E 68 3E 0D   #include <io.h>.
00000010  0A 23 69 6E 63 6C 75 64 65 20 3C 73 79 73 5C 73   .#include <sys\s
00000020  74 61 74 2E 68 3E 0D 0A 23 69 6E 63 6C 75 64 65   tat.h>..#include
00000030  20 3C 73 79 73 5C 74 79 70 65 73 2E 68 3E 0D 0A    <sys\types.h>..
00000040  23 69 6E 63 6C 75 64 65 20 3C 73 74 64 6C 69 62   #include <stdlib
00000050  2E 68 3E 0D 0A 23 69 6E 63 6C 75 64 65 20 3C 65   .h>..#include <e
    :                            :                                  :
    :                            :                                  :
    :                            :                                  :
    :                            :                                  :
00000970  20 20 63 6C 6F 73 65 28 20 66 64 49 6E 20 20 29     close( fdIn  )
00000980  3B 0D 0A 20 20 20 20 63 6C 6F 73 65 28 20 66 64   ;..    close( fd
00000990  4F 75 74 20 29 3B 0D 0A 7D 0D 0A                  Out );..}..


低水準入出力関数

13-10.標準入出力

問題

    MS-DOSのDUMPコマンドを作れ。DUMPコマンドとは、コマンドラインからパラメータを １つとり、パラメータで指定されたファイルを16進数で表示するプログラムである。

    下図のように「アドレスフィールド」「16進数フィールド」「ASCIIフィールド」から 構成されており、ASCIIフィールドは、対応する文字を表示する。ただし、0x20未満と 0x7F以上は'.'を表示する。

00000000  23 69 6E 63 6C 75 64 65 20 3C 69 6F 2E 68 3E 0D   #include <io.h>.
00000010  0A 23 69 6E 63 6C 75 64 65 20 3C 73 79 73 5C 73   .#include <sys\s
00000020  74 61 74 2E 68 3E 0D 0A 23 69 6E 63 6C 75 64 65   tat.h>..#include
00000030  20 3C 73 79 73 5C 74 79 70 65 73 2E 68 3E 0D 0A    <sys\types.h>..
00000040  23 69 6E 63 6C 75 64 65 20 3C 73 74 64 6C 69 62   #include <stdlib
00000050  2E 68 3E 0D 0A 23 69 6E 63 6C 75 64 65 20 3C 65   .h>..#include <e
    :                            :                                  :
    :                            :                                  :
    :                            :                                  :
    :                            :                                  :
00000970  20 20 63 6C 6F 73 65 28 20 66 64 49 6E 20 20 29     close( fdIn  )
00000980  3B 0D 0A 20 20 20 20 63 6C 6F 73 65 28 20 66 64   ;..    close( fd
00000990  4F 75 74 20 29 3B 0D 0A 7D 0D 0A                  Out );..}..

解答

#include <io.h>
#include <sys\stat.h>
#include <sys\types.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <conio.h>

void main(int argc, char *argv[]){
    int   fd;                    // ファイルディスクリプタ
    int   address = 0;           // アドレス
    int   ret;                   // read関数の戻り値
    unsigned char  data[16];     // 読み込んだ文字

    // パラメータが少ない場合
    if ( argc < 2 ){
        printf("パラメータが足りません.\n\n");
        return;
    }

    // パラメータが多い場合
    if ( argc > 3 ){
        printf("パラメータが多すぎます.\n\n");
        return;
    }

    // パラメータは「/?」か?
    if ( strcmp(argv[1], "/?" ) == 0 ){
        printf("ファイルの16進数ダンプを表示します.\n\n");
        printf("MYDUMP [ドライブ:][パス]ファイル名\n\n");
        return;
    }

    // ファイルオープン
    fd = open(argv[1], O_RDONLY | O_BINARY  );

    if ( fd == -1 ){
        switch ( errno ){
        case EACCES :
            printf("アクセスは拒否されました");
            break;
        case EMFILE :
            printf("開いているファイルが多すぎます");
            break;
        default :
            printf("ファイルが見つかりません.");
        }
        printf(" - %s\n\n", argv[1]);
        return;
    }

    // 16バイト読む
    while( (ret = read(fd, data, sizeof(data))) > 0 ){
        int i;

        // アドレス表示
        printf("%08X : ", address);

        // データ表示
        for ( i = 0 ; i < ret ; i++ ){
            int tmp = data[i];
            printf("%02X ", tmp);
        }

        // 空白表示
        for ( i = sizeof(data) - ret ; i > 0 ; i-- )
            printf("   ");

        // スペース
        printf("  ");

        for ( i = 0 ; i < ret ; i++ ){
            if ( data[i] >= 0x20 && data[i] <= 0x7E )
                printf("%c", data[i]);
            else
                printf(".");
        }

        // 改行
        printf("\n");

        address += ret;
    }

    // ファイルクローズ
    close( fd );
}


データ構造

第14章.データ構造

14-1.データ構造とは

    この章ではデータ構造について解説します。本来ならばC++言語で書きたいのですが、 このページはC言語のページなので、C言語で書きます。

    データ構造とはデータを格納する場所を提供する構造で、様々な種類があり、 用途によって使い分ける必要があります。それはそれぞれ速度やコード量 (プログラムの行数)などが異なるためで、用途に応じて最適な物を選ぶ必要が あります。

    このデータ構造の考え方は、C言語に限らず様々な言語にも応用できます。したがって 情報処理技術者試験にも出題されることが多いのです。このため、C言語の勉強とは離れる部分も 多くなりますが、ご了承ください。

    さて、データ構造について解説する前に、必要な関数を紹介します。メモリー操作用の 関数です。

メモリー確保
関数名 | malloc
ヘッダーファイル stdlib.h
malloc.h
alloc.h
のいづれか
書　式 | void* malloc( size_t size )
戻り値 NULL(0)：エラー
それ以外：確保されたメモリーのアドレス
機　能 sizeバイトのメモリーを確保します。この関数を用いて確保されたメモリーはfree 関数を用いて解放しなければなりません。
また、実際に使用されるメモリーは管理用メモリーなどを 必要としますので、残りメモリー量はsizeバイトよりも少なくなります。

メモリー解放
関数名 | free
ヘッダーファイル stdlib.h
malloc.h
alloc.h
のいづれか
書　式 | void free( void* memblock )
戻り値 | なし
機　能 | mallocで割り当てたメモリー領域を開放します。

メモリー初期化
関数名 | memset
ヘッダーファイル memory.h
string.h
のいづれか
書　式 | void* memset( void* dest, int c, size_t count )
戻り値 | destの値
機　能 | destからcountバイトのメモリー領域をcで埋めます。

メモリーコピー
関数名 | memcpy
ヘッダーファイル memory.h
string.h
のいづれか
書　式 | void* memcpy( void* dest, const void* src, size_t count )
戻り値 | destの値
機　能 | srcからcountバイトのメモリー領域をdestから始まる領域にコピーします。


データ構造

トップ-> C言語入門：データ構造-> 14-2. データ構造の種類

14-2.データ構造の種類

    データ構造には以下のような種類があります。それぞれは各ページで紹介します。 (クリックすると該当ページにジャンプします。)
データ構造の種類
← 前ページへ　:　 トップへ　:　 次ページへ →


データ構造

14-3.線形リスト

    線形リストとは、データA1, A2, A3, ....., Anのようなデータがバラバラに保存されており、 A1は次のA2のデータの保管場所を知っていて、A2はA3のデータを.....An-1はAnのデータの保管場所 を知っているという構造です。(C, C++では「保管場所」=「ポインタ」です。)
    ここで、A1, A2, A3, ....., Anを保管している領域は、malloc関数などで 新しく確保したメモリー領域です。

    このようにデータを保管する構造を線形リストと言い、上の 例を特に単方向リストと言います。 次のデータだけでなく、前のデータも知っている構造を 双方向リストと言います。 そして、最後のデータが最初のデータを知っている構造を 環状リストと言います。

    一般にリストは、最初もしくは最後にデータを追加する場合は一定時間ですみますが、 リストの途中にデータを挿入しようとすると時間がかかります。と言っても、配列では 物理的にデータをずらす必要があるのに対して、線形リストはポインタのつなぎ換え だけで良いので、配列よりも高速です。

    また、データの参照もリストの最初もしくは最後は一定時間ですむのですが、 リストの途中のデータにアクセスしようとすると時間がかかります。


データ構造

14-4.単方向リスト

    単方向リストは、一方向リストとも言います。
    上の例のように、プログラム内に最初のデータのポインタを保存しておき、 最初のデータ内に次のデータのポインタを記録します。次のデータにはさらに その次のデータのポインタを保管し、最後のデータは「次のポインタはNULL」と 保管します。(場合によっては次のデータは自分自身と覚える場合もある)

    次のデータのポインタがNULLというのは良くやる方法です。NULLというのは 実際には0を意味しますが、ほとんどのコンピュータでは0番地はシステム領域で、 プログラムでは使用できませんので、「0だったら最後」と判断しても問題が 起こらないからです。

    また、一般にはプログラム内に最初のデータのポインタだけでなく、 最後のデータのポインタも保管しておきます。これはリストの最後にデータを 付け加えようとした場合に、処理速度を向上させるためです。最後のデータの ポインタがプログラムでわからないと、最初のデータから全てのデータを経由して 最後のデータに到達し、新しく付け加えたデータのポインタを格納しなければならず 時間がかかってしまいます。もしプログラムが最後のデータのポインタを知っていれば、 最後のデータを探し出す時間がかかりません。

    では次のページでは、単方向リストの例を紹介します。


データ構造

例題

    この単方向リスト構造を用いて、char型の文字と、次のデータのポインタを格納する 構造体を定義する。そして、main関数で、キーボードから入力されたアルファベット をリスト構造として格納する。

    そのほかに、このリストを操作する関数群を作れ。関数群は以下の通り。

void addhead(struct c_list newlist); | リストの最初にデータを追加する
void addtail(struct c_list newlistt); | リストの最後にデータを追加する
void removeall(); | リストの全てを削除する
struct c_list removetail(); | リストの最後のデータを削除する
struct c_list removehead(); | リストの最初のデータを削除する
struct c_list getat(int nt); | n番目のデータを取得する
void setat(int n, struct c_list newdatat); | n番目のデータをnewdataで置き換える
void removeat(int n); | n番目のデータを削除する
int getcount(); | リストのよう素数を返す
int empty(); | リストが空かどうかを調べる
void insertafter(int n, struct c_list newdata); | n番目の後にnewdataを追加する
void insertbefore(int n, struct c_list newdata); | n番目の前にnewdataを追加する
int finddata(struct c_list data); | リスト内にdataがあれば、何番目かを返す
struct c_list getnext(); | 順次アクセス：次の要素を返す
void getheadposition(); | 順次アクセス：getnextを呼ぶ前に呼ぶ。

解答

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <conio.h>

//
// 構造体の定義
struct c_list{
    char   ch;               // 格納文字
    struct c_list *pnext;    // 次のデータのポインタ
};

// プロトタイプ宣言
void   addhead(struct c_list newlist);
void   addtail(struct c_list newlist);
void   removeall();
struct c_list removetail();
struct c_list removehead();
struct c_list getat(int n);
void   setat(int n, struct c_list newdata);
void   removeat(int n);
int    getcount();
int    empty();
void   insertafter(int n, struct c_list newdata);
void   insertbefore(int n, struct c_list newdata);
int    finddata(struct c_list data);
struct c_list getnext();
void   getheadposition();

//
// リストの最初と最後のデータのポインタ
struct  c_list *pstart, *pend;
struct  c_list *ptr;

//
// メイン関数
void main(){
    struct  c_list data;
    char    ch;

    // リスト作成
    ch = getche();
    while( isalpha(ch) ){
        data.ch = ch;
        addtail(data);
        ch = getche();
    }

    // 改行
    printf("\n");

    // リスト表示
    getheadposition();
    do{
        data = getnext();
        if ( data.ch )
            putch(data.ch);
    }while(data.ch);

    printf("\n");

    // 確保した領域を開放する
    removeall();
}

//
// リストの最後に追加
void addtail(struct c_list newlist){
    struct    c_list *ptr;

    // メモリー確保
    ptr = (struct c_list*)malloc(sizeof(struct c_list));

    // 現在の最後のデータにアドレス格納
    if ( pend )
        pend->pnext = ptr;
    else
        pstart = ptr;
    pend = ptr;

    // データをコピー
    *pend = newlist;

    // 追加したデータの次のポインタをNULLにする
    pend->pnext = NULL;
}

//
// リストの最初に追加
void addhead(struct c_list newlist){
    struct c_list *ptr;

    // メモリー確保
    ptr = (struct c_list*)malloc(sizeof(struct c_list));

    // データをコピー
    *ptr = newlist;

    // 追加するデータの次のポインタを、現在の最初のデータにする
    ptr->pnext = pstart;

    // 最初のデータを、今追加したデータに置き換える
    pstart = ptr;
}

//
// すべてのデータを解放する
void removeall(){
    struct c_list *p, *pnext;

    // １つずつ削除していく
    for ( p = pstart ; p ; p = pnext ){
        pnext = p->pnext;
        free( p );
    }

    // リストの開始、終了位置をNULLにする
    pend = pstart = NULL;
}

//
// 最初のデータを削除する
// 戻り値として、最初のデータを返す
struct c_list removehead(){
    struct c_list ret;

    // 構造体の中身をNULLで初期化する
    ret.ch = '\0';

    // リストの要素が空でなければ
    if ( pstart ){

        // 最初のデータ取得
        ret = *pstart;

        // 最初のデータを解放する
        free( pstart );

        // 最初のデータを次のデータに置き換える
        pstart = ret.pnext;

        // 空になったらpendも修正
        if ( !pstart )
            pend = NULL;
    }
    ret.pnext = NULL;
    return ret;
}

//
// 最後のデータを削除する
// 戻り値として、最後のデータを返す
struct c_list removetail(){
    struct c_list ret;

    // 戻り値用構造体をNULLで初期化する
    ret.ch = '\0';

    // リストの要素が空でなければ
    if ( pend ){
        struct c_list *ptr;

        // 最後のデータ取得
        ret = *pend;

        // 最後から２番目のpnextを変更する
        for ( ptr = pstart ; ptr ; ptr = ptr->pnext ){
            // 最後から２番目のデータなら
            if ( ptr->pnext == pend )
                // pnextをNULLに変更
                ptr->pnext = NULL;
        }

        // 最後のデータを解放する
        free ( pend );

        // 最後のデータを、その前のデータに置き換える
        pend = ptr;

        // 空になったらpstartも書き換える
        if ( !pend )
            pstart = NULL;
    }
    ret.pnext = NULL;
    return ret;
}

//
// n番目のデータを返す
struct c_list getat(int n){
    struct c_list *ptr;
    struct c_list ret;

    // 戻り値用データをNULLで初期化する
    ret.ch    = '\0';
    ret.pnext = NULL;

    // nがマイナスはありえない
    if ( n < 0 )
        return ret;

    // n番目の要素を返す
    for ( ptr = pstart ; ptr ; ptr = ptr->pnext ){
        if ( n-- == 0 )
            return *ptr;
    }

    // n番目の要素がない場合
    return ret;
}

//
// n番目の要素を置き換える
void setat(int n, struct c_list newdata){
    struct c_list *ptr;
    struct c_list *pnext;

    // nがマイナスはありえない
    if ( n < 0 )
        return;

    for ( ptr = pstart ; ptr ; ptr = ptr->pnext ){
        if ( n-- == 0 ){
            pnext = ptr->pnext;
            *ptr = newdata;
            ptr->pnext = pnext;
            return;
        }
    }
}

//
// n番目の要素を削除する
void removeat(int n){
    struct c_list *ptr, *ppre;

    // nがマイナスはありえない
    if ( n < 0 )
        return;

    // 0番目だったら、removeheadを呼び出して帰る
    if ( n == 0 ){
        removehead();
        return;
    }

    // リストが空だったら帰る
    if ( !pstart )
        return;

    // 準備
    ppre = pstart;
    ptr  = pstart->pnext;
    n--;

    while( ptr ){
        // 削除するぞ!
        if ( n-- == 0 ){
            // リストのつなぎ換え
            ppre->pnext = ptr->pnext;

            // n番目のリストを解放する
            free ( ptr );

            return;
        }
        ppre = ptr;
        ptr  = ptr->pnext;
    }

    // n番目がない場合はここに来る
}

//
// リストの要素数を返す
int getcount(){
    struct c_list *ptr;
    int n = 0;

    // 空ではない場合
    if ( pstart ){
        for ( ptr = pstart ; ptr ; ptr = ptr->pnext )
            n++;
    }
    return n;
}

//
// リストが空かどうかを調べる
// 戻り値：0 ------> 空ではない
//         0以外 --> 空である
int empty(){
    return pstart ? 0 : 1;
}

//
// n番目の次に要素を追加する
void insertafter(int n, struct c_list newdata){
    struct c_list *ptr, *pnew;

    // nがマイナスはありえない
    if ( n < 0 )
        return;

    for ( ptr = pstart ; ptr ; ptr = ptr->pnext ){
        // 指定要素なら
        if ( n-- == 0 ){
            // メモリー確保
            pnew = (struct c_list*)malloc(sizeof(struct c_list));

            // 新しく確保したメモリーにデータをコピーする
            *pnew = newdata;

            // リストのつなぎ換え
            pnew->pnext = ptr->pnext;
            ptr->pnext = pnew;

            return;
        }
    }
    // n番目のデータがなければ、ここに来る
}

//
// n番目の前に要素を追加する
void insertbefore(int n, struct c_list newdata){
    struct c_list *ptr, *pnew;

    // nがマイナスはありえない
    if ( n < 0 )
        return;

    // 最初に追加するなら、addheadを呼んで帰る
    if ( n == 0 ){
        addhead( newdata );
        return;
    }

    for ( ptr = pstart ; ptr ; ptr = ptr->pnext ){
        // 指定要素なら
        if ( --n == 0 ){
            // メモリー確保
            pnew = (struct c_list*)malloc(sizeof(struct c_list));

            // 新しく確保したメモリーにデータをコピーする
            *pnew = newdata;

            // リストのつなぎ換え
            pnew->pnext = ptr->pnext;
            ptr->pnext = pnew;

            return;
        }
    }
    // n番目のデータがなければ、ここに来る
}

//
// 要素を探し出す
// 見つからなければ-1を返す
int finddata(struct c_list data){
    struct c_list *ptr;
    int i = 0;

    for ( ptr = pstart ; ptr ; ptr = ptr->pnext ){
        // 見つかった!
        if ( data.ch == ptr->ch )
            return i;
        i++;
    }

    // 見つからなかった
    return -1;
}

//
// 順次アクセス
// 現在のデータをptrで保存しておき、この関数が呼ばれる度に次のデータを返すようにする
struct c_list getnext(){
    struct c_list ret;

    // 初期化
    ret.ch = '\0';

    if ( ptr ){
        ret = *ptr;
        ptr = ptr->pnext;
    }
    ret.pnext = ret.ppre = NULL;
    return ret;
}

//
// 順次アクセス
// 現在のデータをpstartにし、次回のgetnextで最初のデータを返すように初期化する
void getheadposition(){
    ptr = pstart;
}

    このように、リストに関する一連の関数群をあらかじめ作成しておけば、 実際にリストを使う関数(この場合はmain関数)は、リスト構造がどうなっているのかを 知っている必要はない。

    また、このような関数群を作る場合は、可能な限り構造体の中身が変わっても 変更する必要がないように作るべきである。上の例では構造体の中身が、char型１つの データから、メンバーを増やしたりしても、リスト操作用の関数群には変更する必要は ほとんどない(唯一finddata関数だけ変更する必要がある)。


データ構造

14-5.双方向リスト

    単方向リストでは、次のデータを取得する場合には楽でしたが、１つ戻るといった場合は、 また一番最初のデータから探し始めなければなりませんでした。

    双方向リストでは、「次のデータのポインタ」に加えて、「前のデータのポインタ」も 保持していますので、１つ戻るといった操作も行うことができます。

    最初のデータの「前のデータのポインタ」は、一般にNULL(0)が格納されますが、 自分自身の場合もあります。また、最後のデータの「次のデータのポインタ」は、 一般にNULL(0)が格納されますが、自分自身の場合もあります。

    では次のページでは、双方向リストの例を紹介します。


データ構造

例題

    この双方向リスト構造を用いて、char型の文字と、次のデータのポインタと 前のデータのポインタを格納する構造体を定義する。そして、main関数で、 キーボードから入力されたアルファベットをリスト構造として格納する。

    そのほかに、このリストを操作する関数群を作れ。関数群は以下の通り。

void addhead(struct c_list newlist); | リストの最初にデータを追加する
void addtail(struct c_list newlistt); | リストの最後にデータを追加する
void removeall(); | リストの全てを削除する
struct c_list removetail(); | リストの最後のデータを削除する
struct c_list removehead(); | リストの最初のデータを削除する
struct c_list getat(int nt); | n番目のデータを取得する
void setat(int n, struct c_list newdatat); | n番目のデータをnewdataで置き換える
void removeat(int n); | n番目のデータを削除する
int getcount(); | リストのよう素数を返す
int empty(); | リストが空かどうかを調べる
void insertafter(int n, struct c_list newdata); | n番目の後にnewdataを追加する
void insertbefore(int n, struct c_list newdata); | n番目の前にnewdataを追加する
int finddata(struct c_list data); | リスト内にdataがあれば、何番目かを返す
struct c_list getnext(); | 順次アクセス：次の要素を返す
void getheadposition(); | 順次アクセス：getnextを呼ぶ前に呼ぶ。
struct c_list getbefore(); | 順次アクセス：前の要素を返す
void gettailposition(); | 順次アクセス：getbeforeを呼ぶ前に呼ぶ。

解答

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <conio.h>

//
// 構造体の定義
struct c_list{
    char   ch;               // 格納文字
    struct c_list *ppre;     // 前のデータのポインタ
    struct c_list *pnext;    // 次のデータのポインタ
};

// プロトタイプ宣言
void   addhead(struct c_list newlist);
void   addtail(struct c_list newlist);
void   removeall();
struct c_list removetail();
struct c_list removehead();
struct c_list getat(int n);
void   setat(int n, struct c_list newdata);
void   removeat(int n);
int    getcount();
int    empty();
void   insertafter(int n, struct c_list newdata);
void   insertbefore(int n, struct c_list newdata);
int    finddata(struct c_list data);
struct c_list getnext();
void   getheadposition();
struct c_list getbefore();
void   gettailposition();

//
// リストの最初と最後のデータのポインタ
struct  c_list *pstart, *pend;
struct  c_list *ptr;

//
// メイン関数
void main(){
    struct  c_list data;
    char    ch;

    // リスト作成
    ch = getche();
    while( isalpha(ch) ){
        data.ch = ch;
        addtail(data);
        ch = getche();
    }

    // 改行
    printf("\n");

    // リスト表示
    getheadposition();
    do{
        data = getnext();
        if ( data.ch )
            putch(data.ch);
    }while(data.ch);

    printf("\n");

    // 確保した領域を開放する
    removeall();
}

//
// リストの最後に追加
void addtail(struct c_list newlist){
    struct  c_list *ptr;

    // メモリー確保
    ptr = (struct c_list*)malloc(sizeof(struct c_list));

    // 現在の最後のデータにアドレス格納
    if ( pend )
        pend->pnext = ptr;
    else
        pstart = ptr;

    // データをコピー
    *ptr = newlist;

    // 新しく追加したデータのポインタを書き換える
    ptr->ppre  = pend;
    ptr->pnext = NULL;

    // 最後のデータのポインタを追加したデータにする
    pend = ptr;
}

//
// リストの最初に追加
void addhead(struct c_list newlist){
    struct c_list *ptr;

    // メモリー確保
    ptr = (struct c_list*)malloc(sizeof(struct c_list));

    // データをコピー
    *ptr = newlist;

    // 追加するデータのポインタを書き換える
    ptr->pnext = pstart;
    ptr->ppre  = NULL;

    // 最初のデータの「前のデータのポインタ」を書き換える
    if ( pstart )
        pstart->ppre = ptr;

    // 最初のデータを、今追加したデータに置き換える
    pstart = ptr;
}

//
// すべてのデータを解放する
void removeall(){
    struct c_list *p, *pnext;

    // １つずつ削除していく
    for ( p = pstart ; p ; p = pnext ){
        pnext = p->pnext;
        free( p );
    }

    // リストの開始、終了位置をNULLにする
    pend = pstart = NULL;
}

//
// 最初のデータを削除する
// 戻り値として、最初のデータを返す
struct c_list removehead(){
    struct c_list ret;

    // 構造体の中身をNULLで初期化する
    ret.ch = '\0';

    // リストの要素が空でなければ
    if ( pstart ){

        // 最初のデータ取得
        ret = *pstart;

        // 最初のデータを解放する
        free( pstart );

        // 最初のデータを次のデータに置き換える
        pstart = ret.pnext;

        // 前のデータのポインタをNULLにする
        if ( pstart )
            pstart->ppre = NULL;
        else
            pend = NULL;
    }
    ret.pnext = NULL;
    return ret;
}

//
// 最後のデータを削除する
// 戻り値として、最後のデータを返す
struct c_list removetail(){
    struct c_list ret;

    // 戻り値用構造体をNULLで初期化する
    ret.ch = '\0';

    // リストの要素が空でなければ
    if ( pend ){
        struct c_list *ptr;

        // 最後のデータ取得
        ret = *pend;

        // 最後から２番目のpnextを変更する
        if ( pend->ppre )
            pend->ppre->pnext = NULL;

        // 最後のデータを解放する
        free ( pend );

        // 最後のデータを、その前のデータに置き換える
        pend = ret.ppre;

        // リストが空になったら、pstartも書き換える
        if ( !pend )
            pstart = NULL;
    }
    ret.pnext = NULL;
    return ret;
}

//
// n番目のデータを返す
struct c_list getat(int n){
    struct c_list *ptr;
    struct c_list ret;

    // 戻り値用データをNULLで初期化する
    ret.ch    = '\0';
    ret.pnext = NULL;

    // nがマイナスはありえない
    if ( n < 0 )
        return ret;

    // n番目の要素を返す
    for ( ptr = pstart ; ptr ; ptr = ptr->pnext ){
        if ( n-- == 0 )
            return *ptr;
    }

    // n番目の要素がない場合
    return ret;
}

//
// n番目の要素を置き換える
void setat(int n, struct c_list newdata){
    struct c_list *ptr;
    struct c_list *pnext, *ppre;

    // nがマイナスはありえない
    if ( n < 0 )
        return;

    for ( ptr = pstart ; ptr ; ptr = ptr->pnext ){
        if ( n-- == 0 ){
            pnext = ptr->pnext;
            ppre  = ptr->ppre;
            *ptr = newdata;
            ptr->pnext = pnext;
            ptr->ppre  = ppre;
            return;
        }
    }
}

//
// n番目の要素を削除する
void removeat(int n){
    struct c_list *ptr, *ppre;

    // nがマイナスはありえない
    if ( n < 0 )
        return;

    // 0番目だったら、removeheadを呼び出して帰る
    if ( n == 0 ){
        removehead();
        return;
    }

    // リストが空だったら帰る
    if ( !pstart )
        return;

    // 準備
    ppre = pstart;
    ptr  = pstart->pnext;
    n--;

    while( ptr ){
        // 削除するぞ!
        if ( n-- == 0 ){
            // リストのつなぎ換え
            ppre->pnext = ptr->pnext;
            if ( ptr->pnext )
                ptr->pnext->ppre = ppre;
            else
                pend = ppre;

            // n番目のリストを解放する
            free ( ptr );

            return;
        }
        ppre = ptr;
        ptr  = ptr->pnext;
    }

    // n番目がない場合はここに来る
}

//
// リストの要素数を返す
int getcount(){
    struct c_list *ptr;
    int n = 0;

    // 空ではない場合
    if ( pstart ){
        for ( ptr = pstart ; ptr ; ptr = ptr->pnext )
            n++;
    }
    return n;
}

//
// リストが空かどうかを調べる
// 戻り値：0 ------> 空ではない
//         0以外 --> 空である
int empty(){
    return pstart ? 0 : 1;
}

//
// n番目の次に要素を追加する
void insertafter(int n, struct c_list newdata){
    struct c_list *ptr, *pnew;

    // nがマイナスはありえない
    if ( n < 0 )
        return;

    for ( ptr = pstart ; ptr ; ptr = ptr->pnext ){
        // 指定要素なら
        if ( n-- == 0 ){
            // メモリー確保
            pnew = (struct c_list*)malloc(sizeof(struct c_list));

            // 新しく確保したメモリーにデータをコピーする
            *pnew = newdata;

            // リストのつなぎ換え
            pnew->pnext = ptr->pnext;
            pnew->ppre  = ptr;
            ptr->pnext  = pnew;
            if ( pnew->pnext )
                pnew->pnext->ppre = pnew;

            return;
        }
    }
    // n番目のデータがなければ、ここに来る
}

//
// n番目の前に要素を追加する
void insertbefore(int n, struct c_list newdata){
    struct c_list *ptr, *pnew;

    // nがマイナスはありえない
    if ( n < 0 )
        return;

    // 最初に追加するなら、addheadを呼んで帰る
    if ( n == 0 ){
        addhead( newdata );
        return;
    }

    for ( ptr = pstart ; ptr ; ptr = ptr->pnext ){
        // 指定要素なら
        if ( --n == 0 ){
            // メモリー確保
            pnew = (struct c_list*)malloc(sizeof(struct c_list));

            // 新しく確保したメモリーにデータをコピーする
            *pnew = newdata;

            // リストのつなぎ換え
            pnew->pnext = ptr->pnext;
            ptr->pnext  = pnew;
            pnew->ppre  = ptr;
            if ( pnew->pnext )
                pnew->pnext->ppre = pnew;

            return;
        }
    }
    // n番目のデータがなければ、ここに来る
}

//
// 要素を探し出す
// 見つからなければ-1を返す
int finddata(struct c_list data){
    struct c_list *ptr;
    int i = 0;

    for ( ptr = pstart ; ptr ; ptr = ptr->pnext ){
        // 見つかった!
        if ( data.ch == ptr->ch )
            return i;
        i++;
    }

    // 見つからなかった
    return -1;
}

//
// 順次アクセス
// 現在のデータをptrで保存しておき、この関数が呼ばれる度に次のデータを返すようにする
struct c_list getnext(){
    struct c_list ret;

    // 初期化
    ret.ch = '\0';

    if ( ptr ){
        ret = *ptr;
        ptr = ptr->pnext;
    }
    ret.pnext = ret.ppre = NULL;
    return ret;
}

//
// 順次アクセス
// 現在のデータをpstartにし、次回のgetnextで最初のデータを返すように初期化する
void getheadposition(){
    ptr = pstart;
}

//
// 順次アクセス
// 現在のデータをptrに保存しておき、この関数が呼ばれる度に前のデータを返す
struct c_list getbefore(){
    struct c_list ret;

    // 初期化
    ret.ch = '\0';

    if ( ptr ){
        ret = *ptr;
        ptr = ptr->ppre;
    }
    ret.pnext = ret.ppre = NULL;
    return ret;
}

//
// 順次アクセス
// 現在のデータをpendにし、次回のgetbeforeで最後のデータを返すように初期化する
void gettailposition(){
    ptr = pend;
}

    単方向リストの例と比較すると、main関数には 一切の手を加えていない。このようにデータ構造とそれを使う関数で、インターフェース (使い方)をあらかじめ決めておくことで、リスト構造が単方向リスト、双方向リスト などに変更があったとしても、それを使う関数内の変更はない。これは大事な考え方で、 この単方向リストや双方向リストは他のプログラムにそのまま利用できると言うことで、 次回リストを使うプログラムを書こうとしたときに、同じ物を書かなくても良いという ことになる。これをプログラムの再利用性が高いと良い、一般に良いプログラム設計である。


データ構造

14-6.環状リスト

    一般に環状リストは最初のデータという概念はありません。例えばリングバッファーという バッファーに使用します。リングバッファーは主に最新のデータいくつかを保存するという 目的で使われます。

    上の例は要素数4個の環状リストです。A1～A2の「次のデータ」は常に次のデータの ポインタを指していて、データを代入してもそのポインタは変わりません。 「最新のポインタ」がA1を指しているので、さらに新しいデータを代入したい場合は、 A2に入れ、「最新のポインタ」をA2に更新します。さらにA3,A4にデータを代入し、 次のデータを代入したい場合は元に戻って、A1に代入します。これを繰り返すことによって、 最新のデータ4つが常にリストにはいることになります。

    データを取り出したい場合は、「最新のポインタ」からデータを取り出せば、 最新のデータがいつでも取り出せることになります。

    なぜこんな面倒なことをするかというと、マルチスレッドになったときに必要に なります。マルチスレッドプログラムでは、データを代入している最中にも最新データを 取得する場合があります。しかしデータを代入中にそのデータを取得すると矛盾が生じる ことがあります。もしリングバッファーがないと、代入中はデータの取得を待たなければ なりません。このデータが大きい場合や、データの取得は最優先に行わなければ ならない場合は不都合です。そこでリングバッファーを用意して、データの代入中は その前のデータを取得するという方法をとるわけです。 　　では次のページでは、環状リストの例を紹介します。


データ構造

例題

    要素数5個の環状リスト(リングバッファー)を作り、キーボードから入力された 文字をこのリングバッファーに代入する。リターンキーが押されたら、最新の文字を表示する。
void create(int n); | 要素数n個の環状リスト(リングバッファ)を作る
void removeall(); | リストの全てを削除する
void push(struct c_list newdata); | データを代入する
struct c_list pop(); | リストの最初のデータを削除する

解答

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <conio.h>

//
// 構造体の定義
struct c_list{
    char    ch;               // 格納文字
    struct  c_list *pnext;    // 次のデータのポインタ
};

// プロトタイプ宣言
void create(int n);
void removeall();
void push(struct c_list newdata);
struct c_list pop();

//
// リストの最初と最後のデータのポインタ
struct  c_list *pnew;

//
// メイン関数
void main(){
    struct  c_list data;
    char    ch;

    // バッファー作成
    create(5);

    // リスト作成
    ch = getche();
    while( isalpha(ch) ){
        data.ch = ch;
        push(data);
        ch = getche();
    }

    // 改行
    printf("\n");

    // リスト表示
    data = pop();
    printf("%c\n", data.ch );

    printf("\n");

    // 確保した領域を開放する
    removeall();
}

//
// 要素数n個の環状リスト(リングバッファ)を作る
void create(int n){
    struct c_list *ptr;

    // 0以下はありえない
    if ( n <= 0 )
        return;

    // まず現在のリストを全て削除する
    removeall();

    // １つめのバッファーを作る
    pnew = (struct c_list*)malloc(sizeof( struct c_list ));
    pnew->pnext = pnew;

    // ２つめ以降のバッファーを作る
    for ( ptr = pnew ; --n ; ptr = ptr->pnext ){
        ptr->pnext = (struct c_list*)malloc(sizeof( struct c_list ));
    }

    // 最後のバッファーと最初のバッファーをつなげる
    ptr->pnext = pnew;
}

//
// リストの全てを削除する
void removeall(){
    struct c_list *ptr, *pnext;

    // バッファーがなければ帰る
    if ( !pnew )
        return;

    ptr = pnew;
    do{
        pnext = ptr->pnext;
        free( ptr );
        ptr = pnext;
    }while( ptr != pnew && ptr );

    pnew = NULL;
}

//
// データを代入する
void push(struct c_list newdata){
    struct c_list *ptr;

    // データ代入の準備
    ptr   = pnew->pnext;
    newdata.pnext = ptr->pnext;

    // データ代入
    *ptr = newdata;

    // 最新データの更新
    pnew = ptr;
}

//
// リストの最初のデータを削除する
struct c_list pop(){
    return *pnew;
}

    リングバッファーの場合、要素数を変更することはあまりないので、 ベクターを利用してリングバッファーを作る場合も多い。


データ構造

14-7.ベクター

    ベクターとは、配列のことです。リストでは個々の要素はメモリー上で連続しておらず、 次の要素のポインターを保持することで、次の要素を見つけることができました。 ベクターは配列と同じですので、個々の要素は常に隣り合わせで、ベクターは連続した メモリー上に保持されています。

    したがってベクターは、連続した順次アクセスや、ランダムアクセス(○番目のデータを 取得するなど)はリストよりも速く行うことができます。

    しかし、要素数を増やそうとした場合、全部の要素数分のメモリーを再割り当てし、 すべてをコピーしなければなりません(下図)。

    また並び替えを行おうとした場合、リストでは ポインターのつなぎ換えだけで済んだのに対して、ベクターでは物理的にコピーしなければ ならないので、リストより効率が悪くなります。

    したがってベクターは要素数が決まっており、ランダムアクセスを行うデータ に向いています。逆に要素数が頻繁に加減するデータはリストを使うべきです。


データ構造

例題

    名前、電話番号、住所からなる住所録構造体を定義する。これをベクター構造 (要素数５)を用いて管理する。キーボードから入力された名簿を何番目の要素に 追加するかを入力し、ベクターのその要素に格納する。何も入力されなかったら、 0～4番目の要素をディスプレイに表示して終了する。 　　そのほかに、このベクターを操作する関数群を作れ。関数群は以下の通り。

void changebuffersize(int n); バッファーサイズを変更する
増やす場合は現在のバッファーをコピーし、増えた部分は空のままにする
減らす場合は、足りない部分は削除する
int size(); | バッファーサイズを返す
void setat(int n, struct meibo newdata); | ベクターのn番目の要素をnewdataで置き換える
struct meibo getat(int n); | ベクターのn番目の要素を返す

解答

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <conio.h>
#include <memory.h>

//
// 構造体定義
struct meibo{
    char  name[20];
    char  tel[15];
    char  address[30];
};

//
// プロトタイプ宣言
struct meibo input();
void   disp(struct meibo data);

void   changebuffersize(int n);
int    size();
void   setat(int n, struct meibo newdata);
struct meibo getat(int n);

//
// ベクター用の変数
struct meibo *pmeibo;
int    nmeibo;

//
// メイン関数
void main(){
    struct meibo indata;
    int    i;
    int    flag = 1;

    // ベクターを作る
    changebuffersize(5);

    while(flag){
        char buffer[10];

        // 名簿入力
        indata = input();

        if ( indata.name[0] != '\0' ){
            int  no = -1;

            printf("何番目の要素に入力しますか? : ");
            gets(buffer);
            no = atoi(buffer);

            // 要素を代入
            setat(no, indata);
        }

        // 名前が入力されなかったらループから抜ける
        else
            flag = 0;

        // 改行
        printf("\n");
    }

    // 改行
    printf("\n\n");

    // 表示する
    for ( i = 0 ; i < 5 ; i++ )
        disp(getat(i));

    // ベクターを解放する
    changebuffersize(0);
}

//
// バッファーサイズを変更する
void changebuffersize(int n){
    int    currentsize = nmeibo;
    struct meibo *pcurrentbuffer = pmeibo;

    // 新しい領域を作る
    pmeibo = malloc(n * sizeof(struct meibo));

    // 前の領域を新しい領域にコピーする
    if ( currentsize <= n ){
        memcpy(pmeibo, pcurrentbuffer, currentsize * sizeof(struct meibo));

        // 残った領域をきれいにする
        memset(pmeibo + currentsize * sizeof(struct meibo), 0, (n - currentsize) * sizeof(struct meibo));
    }
    else
        memcpy(pmeibo, pcurrentbuffer, n * sizeof(struct meibo));

    // 新しい要素数
    nmeibo = n;
}

//
// バッファーサイズを返す
int size(){
    return nmeibo;
}

//
// ベクターのn番目の要素をnewdataで置き換える
void setat(int n, struct meibo newdata){
    // nが範囲外
    if ( n < 0 || nmeibo < n )
        return;

    // データを代入
    pmeibo[n] = newdata;
}

//
// ベクターのn番目の要素を返す
struct meibo getat(int n){
    // nが範囲外
    if ( n < 0 || nmeibo < n ){
        struct meibo ret;

        // 戻り値用名簿をきれいにする
        memset(&ret, 0, sizeof(struct meibo));

        return ret;
    }

    return *(pmeibo + n);
}

//
// 名簿表示する
void disp(struct meibo data){
    printf("name    : %s\n",   data.name);
    printf("tel     : %s\n",   data.tel);
    printf("address : %s\n\n", data.address);
}

//
// 名簿入力
struct meibo input(){
    struct meibo indata;

    // 戻り値用名簿をきれいにする
    memset(&indata, 0, sizeof(struct meibo));

    printf("名前を入力してください : ");
    gets(indata.name);

    // 名前が入力されなかったら帰る
    if ( indata.name[0] == '\0' )
        return indata;

    printf("電話番号を入力してください : ");
    gets(indata.tel);

    printf("住所を入力してください : ");
    gets(indata.address);

    return indata;
}


データ構造

14-8.木

    木とは、木のように分岐した形をしたデータ構造です。 根(ルート)という親分格のデータ1つあり、その下に複数の枝が 伸びる。枝１つには１つの節(ノード)というデータがあり、 節からやはり複数の枝が伸びて、新たな節というデータがある。逆に言えば節という データには必ず親の節もしくはルートがあることになる。節の下から枝が出ていないような 節のことを葉と呼ぶ。
二分木
    根や節の下に節や葉が２つ以下しかつかないものをいう。完全二分木のことを指す場合もある。

完全二分木
    根や節の下に節や葉が２つしかつかないものをいう。ただ単に二分木という場合もある。

二分検索木
    検索に用いる二分木で、節の左は必ずその節よりも小さい。節の右側は必ずその節よりも 大きい節があるようにした二分木をいう。

多分木
    根や節の下に制限なく節や葉がついているものをいう。ディレクトリとファイルを管理したりするのに使われる。

バランス木
    根から葉までの距離がほぼ等しいものをいう。木は可能な限りバランス木に 近い状態に保つべきである。

ヒープ
    右図のように、ルートの値が一番小さく、階層が深くなるにつれて大きくなるような 木のことを言う。

B木
    通常の二分検索木では、節を昇順(もしくは降順)に挿入すると、右(もしくは左)ばかりに 枝が伸び、木と言うよりはただのリスト構造になってしまう。このような二分検索木は 検索に時間がかかるので、このような状態にならないようにR. Bayer等によって改良された 二分検索木をB木という。


データ構造

14-9.二分検索木

    二分検索木とは検索に用いる二分木で、節の左は必ずその節よりも小さい。 節の右側は必ずその節よりも大きい節があるようにした二分木をいう。その形は バランス木に近い形(根から葉までの距離がほぼ等しい木)の方が検索時間が短くて済む。 要素の追加は比較的短時間で終了するが、要素の削除を行う場合、節や葉のつなぎ換えを しなければならない場合もあるため時間がかかる場合もある。このような理由から、 検索を多く行うデータベースなどに使用され、就業時間が終わるなどして、データーベースへの 追加や検索が行われなくなったときに、メンテナンスとして、バランス木に近い形に つなぎ換えを行う作業を実行する。

要素の追加
    要素の追加は、追加する要素が根よりも小さければ左へ、大きければ右へ進む。 この作業は再帰的に呼び出され、追加すべき葉もしくは節を探す。そしてその葉(節) の左もしくは右に新しい要素を追加する。 二分検索木への追加

    注意点として、昇順(もしくは降順)にデータを追加してはいけない ということです。というのは昇順(降順)に追加すると、バランス木からはずれて、 線形リストと同じ形になってしまい、データの追加、検索の効率が悪くなってしまい ます。

要素の削除
    要素の削除は、追加ほど単純ではない。それは場合によっては節のつなぎ換え作業を 伴う必要があるからである。

    削除する節の枝が０本の場合は、何も考えずにその節を削除すればよい。

    削除する節の枝が１本の場合は、その下の節を削除する節と置き換えればよい。
二分検索木からの削除1

    削除する節の枝が２本の場合は、少し面倒くさい。何度も書いたことだが、節の つなぎ換えが必要になるからだ。例えば下図のように「8」を削除したい場合、 その右に伸びる節(12)を、削除したい節(8)に置き換える。削除したい節(8)の 左に伸びる節(7)を、削除したい節(8)の代わりに置き換えた節(12)から左に 降りていき、左の一番最後の節(10)の左につなげる。 二分検索木からの削除2

    しかしこのようなアルゴリズムで削除するとバランス木の形から離れて、 線形リストに近い木になることも多く、検索効率に影響するため、追加や 削除を何度か行ったら、整理してバランス木に近い形に戻す必要がある。

    以下に、二分検索木の成長の過程シミュレーションプログラムを示します。 Java Appletが表示できる環境でご覧ください。テキストフィールドのところに 数字を入力して、リターンキーもしくは「Insert」ボタンを押すと数字が木に追加されます。 同じくテキストフィールドに数字を入力して「Delete」を押すと、その節が削除されます。 (操作性が悪いですが、ご了承ください)

Java Appletの実行できる環境でおためしください。


データ構造

例題

    学籍番号と名前(と左と右の節へのポインタ)からなる構造体(meibo)を定義する。 キーボードからの名簿入力、名簿削除を行えるようにする。また、学籍番号を 入力すると、名前を検索できるようなプログラムを作れ。

    そのほかに、この木を操作する関数群を作れ。関数群は以下の通り。

int size(); | 木の要素数を返す
int append(struct meibo newdata); | newdataを木に追加する
void removenode(int n); | 学籍番号がnの要素を削除する
void removeall(); | 木を削除する
struct meibo lookup(int n); | 学籍番号がnの要素を検索して返す

解答

#include <stdlib.h>
#include <stdio.h>
#include <memory.h>
#include <conio.h>

//
// 構造体の定義
struct meibo{
    struct meibo *pleft;
    struct meibo *pright;
    int    no;
    char   name[20];
};

//
// 木操作用変数
struct meibo *root;

//
// プロトタイプ宣言
int    size();
int    append(struct meibo newdata);
void   removenode(int n);
void   removeall();
struct meibo lookup(int n);

struct meibo input();
void   disp(struct meibo data);
void   clear();

//
// メイン関数
void main(){
    struct meibo data;
    int    no;
    char   buffer[10];

    while(1){
        // 画面をクリア
        clear();

        printf("メニュー\n");
        printf("   1 : 入力\n");
        printf("   2 : 検索\n");
        printf("   3 : 削除\n\n");
        printf("   0 : 終了\n\n");
        printf("処理を選んでください : ");

        switch(getche()){
        case '1' :
            data = input();
            if ( data.name[0] != '\0' ){
                if ( append(data) )
                    printf("\n\n登録しました\n");
                else
                    printf("\n\nすでに登録されています\n");
            }
            else
                printf("\n\n入力を中止しました\n");
            break;

        case '2':
            printf("\n\n検索する学籍番号を入力してください : ");
            gets(buffer);
            no = atoi(buffer);
            if ( no > 0 ){
                // 検索
                data = lookup(no);
                if ( data.name[0] != '\0' )
                    disp(data);
                else
                    printf("\n\n見つかりませんでした\n");
            }
            else
                printf("\n\n検索を中止しました\n");
            break;

        case '3':
            printf("\n\n削除する学籍番号を入力してください : ");
            gets(buffer);
            no = atoi(buffer);
            if ( no > 0 ){
                // 削除
                removenode(no);
            }
            else
                printf("\n\n削除を中止しました\n");
            break;

        case '4':
        case 0x1B:
            printf("\n\n終了します\n");
            removeall();
            return;
            break;
        }
        getche();
        clear();
    }
}

//
// 名簿を入力する
struct meibo input(){
    struct meibo ret;
    char   buffer[10];
    int    no = -1;

    // 戻り値用名簿をきれいにする
    memset(&ret, 0, sizeof(struct meibo));

    // 改行
    printf("\n\n");

    // 学籍番号入力
    printf("学籍番号 : ");
    gets(buffer);
    no = atoi(buffer);

    // 学籍番号のチェック
    if ( no <= 0 ){
        return ret;
    }

    // 名前入力
    printf("氏　　名 : ");
    gets(ret.name);

    // 名前のチェック
    if ( ret.name[0] == '\0' ){
        return ret;
    }

    ret.no = no;
    return ret;
}

//
// 名簿を表示する
void disp(struct meibo data){
    printf("学籍番号 : %d\n", data.no);
    printf("氏　　名 : %s\n", data.name);

    printf("\n\n");
}

//
// 画面をクリアする
void clear(){
    int i = 0;
    for ( i = 0 ; i < 30 ; i++ )
        printf("\n");
}

//
// 要素の数を数える
// ptrがNULLでなければ、nを１増やし、左側と右側で再帰的に自分を呼び出す
void sizesub(struct meibo *ptr, int *n){
    if ( ptr ){
        (*n)++;
        sizesub(ptr->pleft,  n);
        sizesub(ptr->pright, n);
    }
}

//
// 要素の数を数える
int size(){
    int n = 0;
    sizesub(root, &n);
    return n;
}

//
// 要素追加
int append(struct meibo newdata){
    struct meibo* tmp;

    // 登録する要素をコピーする
    struct meibo* pnewdata = malloc(sizeof(struct meibo));
    *pnewdata = newdata;
    pnewdata->pleft = pnewdata->pright = NULL;

    // 初めての追加
    if ( root == NULL ){
        root = pnewdata;
        return 1;
    }

    // ２回目の追加
    tmp = root;
    while( tmp ){
        // すでに登録済み
        if ( tmp->no == newdata.no ){
            free( pnewdata );
            return 0;
        }

        // 左側
        if ( tmp->no > newdata.no ){
            // 左側にまだ要素がある
            if ( tmp->pleft ){
                tmp = tmp->pleft;
                continue;
            }
            // 左側に登録しよう
            else{
                tmp->pleft = pnewdata;
                return 1;
            }
        }
        // 右側
        else{
            // 右側にまだ要素がある
            if ( tmp->pright ){
                tmp = tmp->pright;
                continue;
            }
            // 右側に登録しよう
            else{
                tmp->pright = pnewdata;
                return 1;
            }
        }
    }
    // ここに来ることはないはずだ
    return 0;
}

//
// つなぎ換えを行う。removenode関数から呼ばれる
void tsunagikae(struct meibo *parent, int n){
    struct meibo **tmp = NULL;
    struct meibo *tmp2, *tmp3;

    if ( parent == NULL ){
        // ありえない
        return;
    }

    // 左側が削除する要素？
    if ( parent->pleft && parent->pleft->no == n )
        tmp  = &parent->pleft;

    // 右側が削除する要素？
    else if ( parent->pright && parent->pright->no == n )
        tmp = &parent->pright;

    // チェック
    if ( !tmp || !(*tmp) ){
        // ありえない
        return;
    }

    // 削除する要素の左側はない?
    if ( !(*tmp)->pleft ){
        // 削除する要素の右側の節を、削除する節の代わりにつなげよう
        *tmp = (*tmp)->pright;
        return;
    }

    // 削除する要素の右側はない?
    if ( !(*tmp)->pright ){
        // 削除する要素の左側の節を削除する節の代わりにつなげよう
        *tmp = (*tmp)->pleft;
        return;
    }

    // 削除する要素には左右２つの枝が伸びている
    tmp2 = (*tmp)->pright;
    *tmp = (*tmp)->pleft;

    tmp3 = *tmp;
    while( tmp3->pright )
        tmp3 = tmp3->pright;

    tmp3->pright = tmp2;
    return;
}

//
// 節を削除する
void removenode(int n){
    struct meibo* tmp;
    struct meibo* pre;

    // 根がなければ削除も何もない
    if ( root == NULL )
        return;

    // 削除は根？
    if ( root->no == n ){
        tsunagikae((struct meibo*)&root, n);
        return;
    }

    tmp = pre = root;
    while( tmp ){
        // 削除はここだ
        if ( tmp->no == n ){
            // ありえない
            return;
        }

        // 左側？
        if ( tmp->no > n ){
            // 左側にもう要素がない？
            if ( !tmp->pleft ){
                // 見つからなかった
                return;
            }
            // 左側は削除する要素？
            else if ( tmp->pleft->no == n ){
                struct meibo *delnode = tmp->pleft;
                tsunagikae(tmp, n);
                free( delnode );
                return;
            }
            // もっと左側を調べる必要がある
            else{
                pre = tmp;
                tmp = tmp->pleft;
                continue;
            }
        }
        // 右側？
        else{
            // 右側にもう要素がない？
            if ( !tmp->pright ){
                // 見つからなかった
                return;
            }
            // 右側は削除する要素？
            else if ( tmp->pright->no == n ){
                struct meibo *delnode = tmp->pright;
                tsunagikae(tmp, n);
                free( delnode );
                return;
            }
            // もっと右側を調べる必要がある
            else{
                pre = tmp;
                tmp = tmp->pright;
                continue;
            }
        }
    }
}

//
// removeall関数から呼ばれる関数で、自分以下を削除する
// 再帰的に自分を呼び出す
void removeallsub(struct meibo* ptr){
    if ( ptr ){
        removeallsub(ptr->pleft);
        removeallsub(ptr->pright);
        free(ptr);
    }
}

//
// 全ての要素を削除する
void removeall(){
    removeallsub(root);
    root = NULL;
}

//
// 学籍番号からmeibo構造体を取得する
struct meibo lookup(int n){
    struct meibo *ptr = root;
    struct meibo ret;

    // 戻り値用構造体をきれいにする
    memset(&ret, 0, sizeof(struct meibo));

    while ( ptr ){
        if ( ptr->no == n )
            return *ptr;

        // 左側にあるはず
        if ( ptr->no > n ){
            // 左側にあるはずなのに、左側がない？
            if ( !ptr->pleft ){
                // みつからない
                return ret;
            }
            else{
                // もっと左を調べる必要がある
                ptr = ptr->pleft;
                continue;
            }
        }
        // 右側にあるはず
        else{
            // 右側に張るはずなのに、右側がない？
            if ( !ptr->pright ){
                // 見つからない
                return ret;
            }
            else{
                // もっと右側を調べる必要がある
                ptr = ptr->pright;
                continue;
            }
        }
    }
    // ここに来るはずはないんだが・・・
    return ret;
}


データ構造

14-10.スタック

    今まで紹介した線形リストやベクター そして木は、物理的なメモリー構造でした。これから紹介する スタックやキューはメモリー構造ではなく、用途で分けた データ構造です。つまり、線形リストを利用したスタックやキュー、ベクターを利用した スタックやキューというものがあります。

    とはいうものの、スタックやキューは要素の数が不定で、端の操作が多いので、 一般には線形リストが使われます。

    前置きはこのくらいにして、スタックについて説明します。

    スタックとは棚のことです。棚と言っても家具屋などで売っている立派な棚ではなくて、 無精な人のアパートを想像してください。雑誌が散乱していますが、床にばらまいておくと 寝るスペースがなくなってしまうので、床に重ねて置いておきます。これがスタックのイメージです。

    重ねた雑誌は上からしか取れません。新しい雑誌を重ねる場合は一番上に置くだけです。 つまり取り出せる雑誌は常に最新号です。古い雑誌を取り出したい場合は、一冊ずつ 別の場所に移動して、取り出したい号を取り出し、その上にあった雑誌(別の場所によけた雑誌) を元通りに重ねます。これがスタックです。このような取り出し方を「LIFO」( Last In First Out)と言います。

    また、スタックに情報を積むことを「pushする」と言い、情報を取り出すことを 「popする」と言います。push, popほど一般的ではありませんが、スタックの 一番上の情報を取り出さずに見るだけのことを「peekする」と言います。

スタックのイメージ

    さて、今までの14章のパターンで行くと、「次ページへ → 」をクリックすると、 例題があるはずですが、今回はありません。というのは、 単方向リストとまったく同じだからです。この単方向リストの例題の 「addhead」と「removehead」を「push」と「pop」に 置き換えて、他の関数を削除した物がスタックです。

    ただし、「removeall」をなくすと、メモリーを解放しないでプログラムを 終了してしまうことになるので注意が必要です(ほとんどのOSではこのようなことがないように 忘れても解放してくれます。)。

    また、「peek」関数に相当する関数は単方向リストにはありませんが、 ここまで呼んでくれた人ならば、すぐに作ることもできると思います。そうです。 ただ単に、「return *pstart;」という１行の関数を作るだけです。


データ構造

14-11.キュー

    スタックでは追加した順とは逆の順で取り出すデータ構造でした。 キューはこのように最初に入れた物は最後に取り出すというLIFOのスタックとは 逆順のデータ構造です。すなわち、最初に入れたデータは最初に取り出すFIFO (First In First Out)の構造です。入れた順番と同じ順番で 取り出すことができます。

    キューにデータを追加することをエンキュー(Enqueue:ENQ)、キューからデータを 取り出すことをデキュー(Dequeue:DEQ)と言います。

スタックのイメージ
(単純)キュー
    (単純)キューは、上図のようにキューの一端からデータを追加(エンキュー)し、 もう一端からデータを取り出(デキュー)します。これを後述するディーキュー に対して「単純キュー」と言います。普通は単に「キュー」と言います。

    単純キューの実装は単方向リストを使用し、 エンキューする場合はaddtailを、デキューする場合にはremovehead関数を 使用し、他の関数(プログラム終了用のremoveall以外)を除くと、単純キューに なります。

ディーキュー
    ディーキューとは、キューのどちらの端にもデータの追加をすることができ、 また、どちらの端からもデータを取り出すことができる特別なキューです。

    例えば、マルチスレッドプログラムで、一方のスレッドがキューに命令を溜めていきます。 そしてもう一方のスレッドがそれを処理していくようなプログラムがあったとします。 命令を実行していく方のスレッドで、「この命令は今実行できない!」と判断したとき、 時間の無駄を省くため、先に別(その次)の命令を実行しようとします。このときに 後で実行するけど、一番最後に回すのはちょっと問題がある場合に、通常はキューの 最後に追加しますが、キューの最初に追加すれば、１回分だけ後回しができるといった 使い方をします。

    ディーキューの実装は双方向リストを使います。双方向リストでは、リストの両端の 追加および削除が単方向リストよりも効率よくできるためです。


データ構造

14-12.マップ

    マップとは辞書のようなものです。イメージ的には辞書や電話帳のようなものです。 見出し語は１つで、その訳語も１つです。同じ見出し語がダブることはありません。 しかし、訳語がダブることはあります。このような辞書のことをマップと言います。

    見出し語はint型でも、文字でも、文字列でも、構造体でも構いません。 訳語も同様に数字でも、文字でも、文字列でも、構造体でも構いません。

    大事なことは見出し語はダブらないということです。

    マップを実際にプログラムすることは面倒なので省略します。簡単なマップは 単方向リストを使えばできます。しかし、一般にマップは 検索に用いることが多いため、単方向リストを使用した簡易マップ構造では、検索の効率が 著しく低下します。そこで、マップに適した構造という物が必要になってきます。

    見出し語を二分検索木にする方法も考えられますが、 一般にはもっと効率の良いデータ構造とアルゴリズムが使用されています。 ハッシュ法(Hashing)と呼ばれる物がそうです。ハッシュ法とはハッシュ関数 (hash function)という関数を用いて、データを一様に分布させる方法で、詳しいことは 本題(C言語)からはずれるので、省略します。ハッシュ関数も多くの種類やアルゴリズムが あり、簡単な(理解しやすい)ものから、効率が良いものなどがあります。

    C++言語など多くの高級言語やそれらのライブラリでは、ここで紹介した リストやベクター、木、スタック、キューそしてマップなどはすでに作られていますので、 データ構造についての詳細な知識はなくてもプログラムすることはできます。 ただし、「こういう用途の時は、あれを使えば効率がいい」 というのさえ覚えていれば(すぐに調べられれば)、本章の目的は達成です。


標準関数

15-1.文字分類・文字変換

        ここで紹介する関数を使用するには「<ctype.h>」を インクルードする必要があります。

文字分類、文字変換
関数名 | 書　式 | 戻り値 | 機　能
isalnum int isalnum(int ch); 0＝アルファベットもしくは数字ではない
0≠アルファベットもしくは数字 		アルファベットもしくは数字かどうかを調べる
isalpha int isalpha(int ch); 0＝アルファベットではない
0≠アルファベット 		アルファベットどうかを調べる
isascii int isascii(int ch); 0＝ASCII文字ではない
0≠ASCII文字 		ASCII文字(0x00～0x7F)かどうかを調べる
iscntrl int iscntrl(int ch); 0＝制御文字ではない
0≠制御文字 		制御文字(0x00～0x1F)かどうかを調べる
isdigit int isdigit(int ch); 0＝数字ではない
0≠数字 		数字('0'～'9')かどうかを調べる
islower int islower(int ch); 0＝小文字ではない
0≠小文字 		小文字('a'～'z')かどうかを調べる
isprint int isprint(int ch); 0＝表示可能文字ではない
0≠表示可能文字ではない 		ディスプレイ(プリンタ)に表示可能な文字かどうかを調べる
ispunct int ispunct(int ch); 0＝句読点文字ではない
0≠句読点文字 		句読点(ピリオド、カンマなど)かどうかを調べる
isspace int isspace(int ch); 0＝スペースではない
0≠スペース 		スペース(タブやスペース)かどうかを調べる
isupper int isupper(int ch); 0＝大文字ではない
0≠大文字 		大文字('A'～'Z')かどうかを調べる
isxdigit int isxdigit(int ch); 0＝16進数値ではない
0≠16進数値 		16進数値(数字か'a'～'f'、'A'～'F')かどうかを調べる
toascii | int toascii(int ch); | ASCII文字を返す | ASCII文字に変換する
tolower | int tolower(int ch); | 小文字を返す | 大文字のアルファベットなら小文字に変換する
toupper | int toupper(int ch); | 大文字を返す | 小文字のアルファベットなら大文字に変換する


標準関数

15-2.文字列操作

    ここでは、文字列を操作したり、比較などを行う関数を紹介します。

    ここで紹介する関数を使用するには「<string.h>」を インクルードする必要があります。

文字列操作関数
関数名 | 書　式 | 戻り値 | 機　能
strcat | char* strcat( char* p1, char* p2 ); | p1の値がそのまま返る | 文字列p1の後に文字列p2をつなげる
strcmp int strcmp( char* p1, char* p2 ); 負・・・文字列p1が文字列p2より小さい
０・・・２つの文字列は同じ
正・・・文字列p1が文字列p2より大きい 		文字列を比較する
strcpy | char* strcpy( char* p1, char* p2 ); | p1の値がそのまま返る | 文字列p2をp1が示すポインタにNULL文字を含めてコピーする
strlen | int strlen( char* p1 ); | 文字列p1の文字数を返す | 文字列の文字数を調べる
strncat char* strncat( char* p1, char* p2, unsigned int n ); p1の値がそのまま返る p1にp2のn文字目までつなげNULL文字を付加する。
文字列p2の長さがnよりも長い場合はp2の長さとする
strncmp int strncmp( char* p1, char* p2, unsigned int n ); 負・・・文字列p1が文字列p2より小さい
０・・・２つの文字列は同じ
正・・・文字列p1が文字列p2より大きい 		文字列をn文字目まで比較する
strncpy char* strcpy( char* p1, char* p2, unsigned int n ); p1の値がそのまま返る p2のn文字目までをp1が示すポインタの位置にコピーする
ただしNULL文字はつけない


標準関数

15-3.データ変換

    ここでは、文字列から数値、逆に数値から文字列への変換を行う関数を紹介します。

    ここで紹介する関数を使用するには「<stdlib.h>」を インクルードする必要があります。

データ変換関数
関数名 | 書　式 | 戻り値 | 機　能
atof | double atof( char* p ); | double型に変換した値 | 文字列をdouble型に変換する
atoi | int atoi( char* p ); | int型に変換した値 | 文字列をint型に変換する
atol | lon atol( char* p ); | long型に変換した値 | 文字列をlong型に変換する
itoa | char* itoa( int n, char* p, int d ); | pの値がそのまま返る | 整数nをd進数の文字列に変換する
ltoa | char* ltoa( long n, char* p, int d ); | pの値がそのまま返る | 整数nをd進数の文字列に変換する
sscanf | int sscanf( const char* p1, const char* p2, ... ); | 入力項目数 | 文字列p1を入力元としてscanfと同じ書式でデータ変換を行う
sprintf | int sprintf( char* p1, const char* p2, ... ); | 出力文字数 | printfと同じ書式でディスプレイの代わりに文字列p1として出力する


標準関数

15-4.メモリー操作

    ここでは、メモリー操作に関する関数を紹介します。

メモリー確保
関数名 | malloc
ヘッダーファイル stdlib.h
malloc.h
alloc.h
のいづれか
書　式 | void* malloc( size_t size )
戻り値 NULL(0)：エラー
それ以外：確保されたメモリーのアドレス
機　能 sizeバイトのメモリーを確保します。この関数を用いて確保されたメモリーはfree 関数を用いて解放しなければなりません。
また、実際に使用されるメモリーは管理用メモリーなどを 必要としますので、残りメモリー量はsizeバイトよりも少なくなります。

関数名 | calloc
ヘッダーファイル stdlib.h
malloc.h
alloc.h
のいづれか
書　式 | void* calloc( size_t num, size_t size )
戻り値 NULL(0)：エラー
それ以外：確保されたメモリーのアドレス
機　能 size×numバイトのメモリーを確保し、0でクリアします。
実際に使用されるメモリーは管理用メモリーなどを 必要としますので、残りメモリー量はsizeバイトよりも少なくなります。

関数名 | realloc
ヘッダーファイル stdlib.h
malloc.h
alloc.h
のいづれか
書　式 | void* realloc( void* memblock, size_t size )
戻り値 NULL(0)：エラー
それ以外：確保されたメモリーのアドレス
機　能 | malloc,calloc,reallocで割り当てたメモリーの大きさをsize バイトに変更します。これにより、メモリー領域の場所(ポインタ)は変わります。 メモリー領域を増やした場合は前のサイズ、メモリー領域を減らした場合は新しいサイズの 内容は同じ値がコピーされます。

メモリー解放
関数名 | free
ヘッダーファイル stdlib.h
malloc.h
alloc.h
のいづれか
書　式 | void free( void* memblock )
戻り値 | なし
機　能 | mallocで割り当てたメモリー領域を開放します。

メモリー初期化
関数名 | memset
ヘッダーファイル memory.h
string.h
のいづれか
書　式 | void* memset( void* dest, int c, size_t count )
戻り値 | destの値
機　能 | destからcountバイトのメモリー領域をcで埋めます。

メモリーコピー
関数名 | memcpy
ヘッダーファイル memory.h
string.h
のいづれか
書　式 | void* memcpy( void* dest, const void* src, size_t count )
戻り値 | destの値
機　能 | srcからcountバイトのメモリー領域をdestから始まる領域にコピーします。

関数名 | memmove
ヘッダーファイル | string.h
書　式 | void* memmove( void* dest, const void* src, size_t count )
戻り値 | destの値
機　能 | memcpyと同等ですが、コピー元とコピー先の一部が重なっている場合でも大丈夫です。

関数名 | memccpy
ヘッダーファイル memory.h
string.h
のいづれか
ただしANSI規格外なので、サポートされていないコンパイラもある
書　式 | void* memccpy( void* dest, const void* src, int c, unsigned int count )
戻り値 | destの値
機　能 | srcから始まるメモリー領域をdestから始まるメモリー領域にコピーします。 sizeバイトコピーするか、cの下位１バイトのデータをコピーしたら終了します。

文字検索
関数名 | memchr
ヘッダーファイル memory.h
string.h
のいづれか
書　式 | void* memchr( const void* buf, int c, size_t count )
戻り値 NULL(0)：含まれていない
cのポインタ
機　能 srcからcountバイトのメモリー領域にcの下位１バイト分のデータが 含まれていれば、そのポインタを返します。
もし含まれていないならば、NULL(0)を返します。 複数存在する場合は、最初のポインタが返されます。

メモリー比較
関数名 | memcmp
ヘッダーファイル memory.h
string.h
のいづれか
書　式 | int memcmp( const void* buf1, const void* buf2, size_t count )
戻り値 NULL(0)：含まれていない
cのポインタ
機　能 buf1から始まるメモリー領域とbuf2から始まるメモリー領域を countバイト比較します。
例えば、0x30,0x31....という領域と 0x30,0x30....という領域を比較した場合、後者のメモリー領域の方が 小さいと言います。

関数名 | memicmp
ヘッダーファイル memory.h
string.h
のいづれか
ただしANSI規格外なので、サポートされていないコンパイラもある
書　式 | int memicmp( const void* buf1, const void* buf2, unsigned int count )
戻り値 NULL(0)：含まれていない
cのポインタ
機　能 | memcmpと同じだが、半角大文字と半角小文字の区別はしません。

※size_tはunsigned intと同じです。


標準関数

15-5.数値演算

    ここでは、数値演算に関する関数を紹介します。数値演算とは、四則演算以外の 数学的な演算、例えば指数計算などのことをいいます。

    これらを使用するためには、「math.h」をインクルードする必要があります。ただし、 abs, labs, rand, srand関数は「stdlib.h」を インクルードする必要があります。

種別 | 関数名 | 書式 | 機能
算
術
関
数 		exp double exp( double x ) 指数 ex
log | double log( double x ) | 対数 logex
log10 | double log10( double x ) | 対数 log10x
sqrt | double sqrt( double x ) | xの平方根(ルート√x)
pow | double pow( double x, double y ) | べき乗 xy
三
角
関
数 		sin double sin( double r ) サイン rの単位はラジアン
cos | double cos( double r ) | コサイン rの単位はラジアン
tan | double tan( double r ) | タンジェント rの単位はラジアン
asin | double asin( double x ) | アークサイン xは-1～+1、戻り値は-π/2～+π/2
acos | double acos( double x ) | アークコサイン xは-1～+1、戻り値は-π/2～+π/2
atan | double atan( double x ) | アークタンジェント xは-1～+1、戻り値は-π/2～+π/2
atan2 | double atan2( double x, double y ) | y/xのアークタンジェント　戻り値は-π～+π
sinh | double sinh( double x ) | ハイパボリックサイン
cosh | double cosh( double x ) | ハイパボリック
tanh | double tanh( double x ) | ハイパボリック
絶
対
値 		abs int abs( int k ) 整数kの絶対値
fabs | double fabs( double x ) | 小数xの絶対値
labs | long fabs( long l ) | long型lの絶対値
乱
数 		rand int rand( ) 0～RAND_MAXまでの疑似乱数を返す
srand void srand( int seed ) 乱数列の初期化
rand関数はある計算に基づいて乱数を作っているため、 rand関数を使ったプログラムを何度呼び出しても同じ結果が得られてしまう。 そこでこの関数を用いて、このようなことが起こらないように、乱数計算の初期値を 返る。一般にseedには時間を用いる場合が多い。
そ
の
他 		ceil double ceil( double k ) 小数点以下切り上げ
floor | double floor( double k ) | 小数点以下切り捨て
fmod | double fmod( double x, double y ) | x÷yの余りを返す
frexp | double frexp( double x, int* expptr ) | 浮動小数点値(x)を仮数(m)と指数(n)に分けて、m の絶対値が0.5以上かつ1.0未満になるようにし、またx=m×(2n)になるようにします。整数値の指数nは、expptrが指す位置に格納されます。
ldexp | double ldexp( double x, int exp ) | x×2expを返す
modf | double ldexp( double x, double* intptr ) | modf関数は、浮動小数点値xをそれと同じ符号を持つ小数部と整数部に分割し、xの小数部を符号付きで返します。整数部は、浮動小数点値としてintptrの指す領域に格納されます。


標準関数

15-6.ファイル操作(高水準入出力関数)

    ここでは、ファイル操作関数を紹介します。詳しくは12章を参照してください。

    ここで紹介する関数を使用するには「&stdio.h>」を インクルードする必要があります。

ファイル作成・オープン

関数名 | fopen
書式 | FILE* fopen( const char* filename, const char* mode )
戻り値 NULL＝エラー
NULL≠FILE構造体へのポインタ
機能 第一引数の渡されたファイル名を
第二引数で渡されたモードでオープンする(作成する)

モード(第二引数) | 動作 | ファイルが存在しない場合 | ファイルが存在する場合
"r" 読込モード エラー
"w" | 書込モード | 新しいファイルを作成 | 内容が失われる
"a" | 追加書込 | 新しいファイルを作成 | 既存ファイルの最後に追加
"r+" 読込・書込モード エラー
"w+" | 読込・書込モード | 新しいファイルを作成 | 内容が失われる
"a+" | 読込・追加書込モード | 新しいファイルを作成 | 既存ファイルの最後に追加

モード(第二引数)に
追加する文字 		モード 機能
t テキストモード 読込時：CR,LF('\r','\n')をLF('\n')に変換する
書込時：LF('\n')をCR,LF('\r','\n')に変換する
b バイナリモード テキストモードのような変換を行わない

ファイル読み込み

関数名 | fgetc
書式 | int fgetc( FILE* fp )
戻り値 EOF：エラーもしくは、ファイル終端
それ以外：読み込んだ文字
機能 ファイルを現在位置から１バイト読み込み、その文字を返す。
ファイルポインタを一つ進める

関数名 | fgets
書式 | char* fgets( char* p, int n, FILE* fp )
戻り値 NULL：エラーもしくは、ファイル終端
それ以外：読み込んだ文字列の格納場所
機能 ファイルを現在位置から「n-1バイト」あるいは、改行(LF='\n')かファイル終端までpに読み込む。
ファイルポインタを読み込んだ分進める。
文字列の最後にはNULLを自動的にふかしてくれる。

関数名 | fscanf
書式 | int fscanf( FILE* fp, const char* format, ... )
戻り値 EOF：エラーもしくは、ファイル終端
それ以外：読み込んだ項目数
機能 第二引数以降はscanf関数と同じです。
読み込んだバイト数だけファイルポインタを進める

関数名 | fread
書式 | size_t fread( void* buffer, size_t size, size_t count, FILE* fp )
戻り値 EOF：エラー
それ以外：実際に読み込んだ項目数
機能 ファイルを現在位置から、１項目sizeバイトのデータをcount個読み込み、bufferに格納する。
戻り値がcountよりも小さい場合は、エラーかファイル終端に到達したことになります。

ファイル書き込み

関数名 | fputc
書式 | int fputc(int ch, FILE* fp )
戻り値 EOF：エラーもしくは、ファイル終端
それ以外：書き込んだ文字
機能 ファイルの現在位置から１バイト書き込み、その文字を返す。
ファイルポインタを一つ進める

関数名 | fputs
書式 | int fputs( char* p, FILE* fp )
戻り値 EOF：エラー
それ以外：書き込んだ最後の文字
機能 ファイルを現在位置へ、pからNULL文字までファイルに書き込む。
ファイルポインタを書き込んだ分進める。
文字列の最後にはNULLを自動的にふかしてくれる。

関数名 | fprintf
書式 | int fprintf( FILE* fp, const char* format, ... )
戻り値 負の値：エラー
それ以外：書き込んだ文字数
機能 第二引数以降はprintf関数と同じです。
書き込んだバイト数だけファイルポインタを進める

関数名 | fwrite
書式 | size_t fwrite( void* buffer, size_t size, size_t count, FILE* fp )
戻り値 戻り値<count：エラー
戻り値=count：正常実行
機能 | bufferに格納されているsizeバイトのデータをcount個書き込み、ファイルポインタを進めます。

ファイルクローズ

関数名 | fclose
書式 | int fclose( FILE* fp )
戻り値 EOF：エラー
0：正常終了
機能 | ファイルをフラッシュし、クローズする

関数名 | fcloseall
書式 | int fcloseall()
戻り値 EOF：エラー
0：正常終了
機能 標準入力(stdin)、標準出力(stdout)、標準エラー出力(stderr)、(MS-DOS系OSでは、 標準補助入出力 (stdaux) と標準プリンタ出力 (stdprn) )を除くファイルをフラッシュしてクローズする。
一時ファイルは削除されます。

関数名 | fflush
書式 | int fflush( FILE* fp )
戻り値 EOF：エラー
0：成功かフラッシュする必要がない
機能 書込モード：実際にディスクに書き込む
読込モード：バッファーをクリアする
FILE構造体のポインタがNULLの場合：すべてのファイルをフラッシュする

エラー関連

関数名 | feof
書式 | int feof( FILE* fp )
戻り値 0：ファイル終端ではない
0以外：ファイル終端
機能 | ファイルの終端まで到達したかどうかを調べる

関数名 | ferror
書式 | int ferror( FILE* fp )
戻り値 0：エラーなし
0以外：エラーが発生している
機能 | 直前のファイル操作でエラーが発生しているかどうかを調べる

関数名 | fclearerr
書式 | void fclearerr( FILE* fp )
戻り値 | なし
機能 | エラーフラグをクリアする

ランダムアクセス

関数名 | ftell
書式 | long ftell( FILE* fp )
戻り値 -1：エラー
-1以外：現在の位置
機能 ファイルポインタの位置を返す
(戻り値が0の時はファイルの最初を示している)

関数名 | fseek
書式 | int fseek( FILE* fp, long offset, int origin )
戻り値 0：エラーなし
0以外：エラーが発生している
機能 ファイルポインタをoriginからoffsetバイトの新しい位置に移動します。 以後の処理は移動したファイルポインタを基点に行われます。

また、originは以下の値を 指定します。
  SEEK_SET・・・ファイルの先頭を基点にする
  SEEK_CUR・・・現在のファイルポインタを基点にする
  SEEK_END・・・ファイルの終端を基点にする

stdio.hに#defineされていますが、コンパイラによっては定義されていない場合があります。 その場合は以下の値を使います。
  0・・・ファイルの先頭を基点にする
  1・・・現在のファイルポインタを基点にする
  2・・・ファイルの終端を基点にする

関数名 | frewind
書式 | void frewind( FILE* fp )
戻り値 0：正常実行
0以外：エラー
機能 | ファイルポインタをファイルの先頭に移し、エラーフラグをクリアします。


標準関数

15-7.ファイル操作(低水準入出力関数)

    ここでは、ファイル操作関数を紹介します。詳しくは13章を参照してください。

    ここで紹介する関数を使用するためには、必要に応じて以下のヘッダーファイルを インクルードしてください。

    #include <io.h>
    #include <sys\stat.h>
    #include <sys\types.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <string.h>
    #include <fcntl.h>
    #include <conio.h>
ファイル作成

関数名 | creat
書式 | int creat( const char* filename, int pmode );
戻り値 -1:エラー
その他:ファイルディスクリプタ
機能 第一引数の渡されたファイル名を
第二引数で渡されたモードでオープンする(作成する)
モード(第二引数) | 動作
S_IREAD | 読み込み専用モード
S_IWRITE | 読み込み・書き込みモード
「S_IREAD」や「S_IWRITE」は「sys\stat.h」で定義されていますので、 このファイルをインクルードする必要があります。
エラーコード | 意味
EACCES | ファイル名に既存の読み出し専用ファイルを指定したか、またはファイルではなくディレクトリを指定しています。
EMFILE | 開いているファイルが多すぎます
「stdlib.h」をインクルードすると、自動的にerrnoというグローバル変数が作られます。 エラーが起きた場合は、この変数にエラーコードが自動的に代入されます。ただし、これら エラーコードは「errno.h」で定義されています。

ファイルオープン

関数名 | open
書式 | int open( const char* filename, int oflag[, int pmode] );
戻り値 -1:エラー
その他:ファイルディスクリプタ
機能 第一引数の渡されたファイル名を
第二引数で渡されたモードでオープンする(作成する)
ただし、ファイル作成モードでは第三引数にファイル属性を指定する
モード(第二引数) | 動作
O_RDONLY | 読み込み専用モード
O_WRONLY | 書き込み専用モード
O_RDWR | 読み込み・書き込みモード
O_CREAT | ファイルの新規作成
O_TRUNC | 既存ファイルをオープンし、中身を空にする
O_EXCL | ファイルが存在すれば、新規作成しない
O_APPEND | 追加書込モード
O_TEXT | テキストモード
O_BINARY | バイナリーモード
上表のモードは「fcntl.h」で定義されているので、これをインクルードする必要がある。
モード(第三引数) | 動作
S_IREAD | 読み込み専用モード
S_IWRITE | 読み込み・書き込みモード
・第三引数は、第二引数が「O_CREAT」の時のみ指定する。
・第二引数の「S_IREAD」や「S_IWRITE」は「sys\stat.h」で定義されていますので、 このファイルをインクルードする必要があります。
エラーコード | 意味
EACCES | ファイル名に既存の読み出し専用ファイルを指定したか、またはファイルではなくディレクトリを指定しています。
EMFILE | 開いているファイルが多すぎます
「stdlib.h」をインクルードすると、自動的にerrnoというグローバル変数が作られます。 エラーが起きた場合は、この変数にエラーコードが自動的に代入されます。ただし、これら エラーコードは「errno.h」で定義されています。

ファイル読み込み

関数名 | read
書式 | int read(int fd, void* buffer, unsigned int count );
戻り値 -1:エラー
0:ファイルの終端で呼ばれた
その他:実際に読み込んだサイズ(バイト)
機能 | オープンされているファイルディスクリプタ(fd)をcountバイト読み込み、bufferに格納する
エラーコード 意味
EBADF ・fdが無効
・ファイルが読み込み用に開かれていない
・ファイルがロックされている
「stdlib.h」をインクルードすると、自動的にerrnoというグローバル変数が作られます。 エラーが起きた場合は、この変数にエラーコードが自動的に代入されます。ただし、これら エラーコードは「errno.h」で定義されています。

ファイル書き込み

関数名 | write
書式 | int write(int fd, void* buffer, unsigned int count );
戻り値 -1:エラー(ディスク容量が足りないなど)
その他:実際に書き込んだサイズ(バイト)
機能 | bufferに格納されている内容をオープンされているファイルディスクリプタ(fd)へcountバイト書き込みます
エラーコード | 意味
EBADF ・fdが無効
・ファイルが書き込み用に開かれていない
ENOSPC | ・ディスク容量が足りない
「stdlib.h」をインクルードすると、自動的にerrnoというグローバル変数が作られます。 エラーが起きた場合は、この変数にエラーコードが自動的に代入されます。ただし、これら エラーコードは「errno.h」で定義されています。

ファイルクローズ

関数名 | close
書式 | int close(int fd );
戻り値 -1:エラー(ファイルディスクリプタが無効)
0:クローズ成功
機能 | 開いているファイルディスクリプタをクローズします
エラーコード 意味
EBADF fdが無効
「stdlib.h」をインクルードすると、自動的にerrnoというグローバル変数が作られます。 エラーが起きた場合は、この変数にエラーコードが自動的に代入されます。ただし、これら エラーコードは「errno.h」で定義されています。

ランダムアクセス

関数名 | lseek
書式 | long lseek(int fd, long offset, int origin );
戻り値 -1:エラー
その他:ファイル先頭からのバイト数
機能 | originの位置からoffsetバイト目に移動します。
モード(第三引数) | 動作
L_SET | ファイルの先頭からoffsetバイト目に移動 (UNIXのみ)
L_XTND | ファイルの終端からoffsetバイト目に移動 (UNIXのみ)
L_INCR | 何もしない(現在位置を知りたい場合) (UNIXのみ)
SEEK_SET | ファイルの先頭からoffsetバイト目に移動
SEEK_CUR | 現在位置からoffsetバイト目に移動
SEEK_END | ファイルの終端からoffsetバイト目に移動
上表のモードは「stdio.h」で定義されているので、これをインクルードする必要がある。
エラーコード | 意味
EBADF | fdが無効
EINVAL | originの値が無効か、またはoffsetの値がファイルの先頭より前
「stdlib.h」をインクルードすると、自動的にerrnoというグローバル変数が作られます。 エラーが起きた場合は、この変数にエラーコードが自動的に代入されます。ただし、これら エラーコードは「errno.h」で定義されています。

その他

関数名 | fdopen
書式 | FILE* fdopen(int fd, const char* mode );
戻り値 NULL(0):エラー
その他:FILE構造体のポインタ(ファイルポインタ)
機能 | ファイルディスクリプタfdを拡張してバッファー入出力機能(ストリーム)を 追加し、以降高水準入出力関数を用いることができるようにします。
モード(第二引数) | 動作 | ファイルが存在しない場合 | ファイルが存在する場合
"r" 読込モード エラー
"w" | 書込モード | 新しいファイルを作成 | 内容が失われる
"a" | 追加書込 | 新しいファイルを作成 | 既存ファイルの最後に追加
"r+" 読込・書込モード エラー
"w+" | 読込・書込モード | 新しいファイルを作成 | 内容が失われる
"a+" | 読込・追加書込モード | 新しいファイルを作成 | 既存ファイルの最後に追加


標準関数

15-8.プロセス関係

    プロセス関係の関数とは、他のプログラムを実行したり、自分を強制的に終了したりする関数です。 これらはANSI規格の他にもMS-DOS専用の関数や、Windows専用、unix用と、OS依存の関数も 多数存在します。ここではANSI規格の物のみ紹介します。

    ここで紹介する関数を利用するためには、「process.h」をインクルードしてください。

関数名 | 書式 | 機能
exit | void exit( int returncode ) | プロセスを終了します
abort | void abort( ) | プロセスを異常終了します
system | int system( const char* process ) | 子プロセスを実行します。実行プログラムのファイル名をprocessに指定します。


関数ポインタ

第16章.関数ポインタ

16-1. 関数ポインタとは

    変数にポインタがあるように、関数にもポインタがあります。ポインタを使用して変数にアクセス できたのと同様に、関数ポインタを利用して関数を呼び出すことができます。

    関数ポインタは、それ単独で使用するとあまりメリットがないのですが、関数ポインタの 配列のようなものをつくると便利な場合があります。

    詳しくは次ページ以降で解説していきますが、以下のように 使用します。

#include <stdio.h>

void display(char* p){
    printf("%s\n", p);
}

void main(){
    void (*p_func)(char*) = display;
    p_func("abcd");
}

abcd
    「p_func」というのが関数ポインタの変数で、この場合、displayを代入しているので、 この「p_func」という関数ポインタは、display関数を表していおり、文字列を表示させる ことができます。


関数ポインタ

16-2. 関数ポインタ

#include <stdio.h>

void display(char* p){
    printf("%s\n", p);
}

void main(){
    void (*p_func)(char*) = display;
    p_func("abcd");
}

abcd
    「p_func」というのが関数ポインタの変数で、この場合、displayを代入しているので、 この「p_func」という関数ポインタは、display関数を表しており、文字列を表示させる ことができます。

    main関数の１行目が関数ポインタを代入するための変数を宣言している部分であり(この場合代入も 同時に行っている)、main関数の２行目で、その関数を呼び出している。

    この宣言は以下のように記述することもできる。

    void (*f1)(char*) = display;
    void (*f2)(char*) = &display;
    また、関数ポインタを利用して関数を呼び出す場合は以下のように記述することもできる。
    f1("This ia a pen");
    (*f1)("This is a pen");


関数ポインタ

16-3. 関数ポインタと引数・戻り値

    関数ポインタは、関数自体と同様、引数の型や戻り値の型を宣言する必要があります。関数ポインタに 関数のアドレスを代入する場合、引数や戻り値が完全に一致している必要があります。

#include <stdio.h>

void (*pf)(char*);    // 戻り値void,  引数char* という関数を代入できる関数ポインタ
void f1(char*);       // f1という関数のプロトタイプ宣言
int  f2(char*);       // f2という関数のプロトタイプ宣言
void f3(int*);        // f3という関数のプロトタイプ宣言

void main(){
    pf = &f1;	// OK
    pf = &f2;	// NG 戻り値が一致しない
    pf = &f3;	// NG 引数が一致しない

    pf("Hello");	// OK
    pf(20);	// NG 引数が一致しない

    int i = pf("Hello");	// NG 戻り値はvoidなのでintに代入できない
}
    上記の例のように、戻り値も引数も同じものでないと代入できない。


関数ポインタ

16-4. 関数ポインタの配列

    関数ポインタも一種の変数なので、配列にすることも可能です。例えばメニューの処理などを作成する 時には重宝します。

    例として住所録を作ります。住所録プログラムはキーボードから住所録を入力し、ファイルにセーブ することができます。当然ファイルをロードすることもできます。その他に表示とソート機能を有します。

#define SIZE 100

// 住所録構造体
struct DATA{
    char   name[20];
    char   address[50];
    char   tel[15];
};

//住所録データ
struct DATA  address_data[SIZE];

// 関数のプロトタイプ宣言
void makenew();     // 新規作成
void data_sort();   // ソート
void display();     // 表示
void save();        // セーブ
void load();        // ロード
void quit();        // 終了
void input();       // データ入力
void delete_data(); // データ入力

// 関数ポインタ
void (*pf[])() = {
    makenew,
    data_sort,
    display,
    save,
    load,
    input,
    delete_data
};

void main(){
    while(1){
        int a = getch();
        a -= '0';
        if( 0 <= a && a <= sizeof(pf) )
            pf[a]();
    }
}

    上記のように、関数ポインタの配列を用意することで、メニューを簡単に作成することができます。 このような方法を用いることのメリットはもう１つあります。それは状態によってメニューを切り替える ことも簡単にできるということです。

    例えば、データが１つもない場合は、セーブもソートも行うことはできません。この場合、メニューも 書き換えてしまいましょう。つまり関数ポインタの配列を書き直せばよいのです。このように場合によって メニューを変えたい場合も、関数ポインタの配列を使えば簡単にできます。

    もし、関数配列のポインタを使用しないで、メニューの書換を行う場合は、メニュー処理の 条件分岐(switch～case文など)の処理が繁雑になってしまうでしょう。

← 前ページへ　:　 トップへ　:　 次ページへ →


C言語入門

    C言語入門へようこそ。

    C、C++のコンパイラは購入すると結構な出費になります。将来プログラマーを目指したり、 シェアウェアーやフリーウェアーを作る人ならまだしも、これから勉強してみようかなと思う 学生がコンパイラを買うのはちょっと痛い出費かもしれません。そこで、フリーで手に入る コンパイラを紹介します。

  • Borland C++ 5.5
  • LSI C-86 Ver.3.30c試食版
  • 多摩ソフトウェア Light C 1.23 体験版
  • 技術評論社、C言語スタートブック 付録CD-ROMなど

    また、私がよく読むC言語の本を紹介します。今手に入るかどうかはわかりませんが。。。

出版社 | タイトル | ISBN | 内容
技術評論社 | C言語による最新アルゴリズム事典 | 4-87408-414-1 | C言語によるアルゴリズム集
ナツメ社 | C言語で学ぶプログラミング基礎の基礎 | 4-8163-3305-3 | 初心者向け
ソフトバンク
パブリッシング 		秘伝C言語問答
ポインタ編 第2版 		4-7973-1826-0 ポインタの正しい理解のために
技術評論社 C言語 関数の使い方+作り方 完全制覇
（標準プログラマーズライブラリ） 		4-7741-1300-X かなり詳しい。初級から中級向け
正しいC言語を身につけたい人向け
本の検索・ご購入は「 コンピュータの本・専門店」で。

←目次から選んでください

・ トップページに戻る


C言語入門

トップ->C言語入門

あなたは

人目のC(C++)言語入門受講生です。

目次
0. はじめに

1. 基本的な決まり
      1. 書式
      2. 予約語
      3. 構成
      4. データの形と宣言
      5. 定数と変数
      6. 簡単な演算

2. 入出力
      1. printf
      2. scanf
      3. 練習問題1
      4. 1文字入出力
      5. エスケープシーケンス

3. 制御文
      1. 順次構造
      2. 単一分岐
      3. 多重分岐
      4. ケース構造
      5. 反復構造(while)
      6. 反復構造(do)
      7. 反復構造(for)
      8. 補助制御文
      9. 練習問題
      10.練習問題

4. 配列
      1. 配列とは
      2. 配列の宣言と初期化
      3. 配列の使用法
      4. 練習問題
      5. 文字列
      6. ２次配列と多次元配列
      7. 練習問題

5. 演算子
      1. 演算子の種類
      2. インクリメント演算子とデクリメント演算子
      3. 複合代入演算子
      4. ビット演算子
      5. シフト演算子
      6. キャスト演算子
      7. 順次演算子
      8. 条件演算子
      9. sizeof演算子
      10.演算子の優先順位
      11.式と値
      12.条件式と値
      13.練習問題

6. ポインタ
      1. ポインタとは
      2. 配列とポインタ
      3. 文字列とポインタ
      4. ポインタのメリット

7. 関数
      1. 関数の作り方
      2. プロトタイプ宣言
      3. 配列とポインタ
      4. 値渡しとアドレス渡し
      5. main関数の引数
      6. 練習問題
      7. 標準関数
      8. 再帰関数

8. プリプロセッサ
      1. プリプロセッサとは
      2. #define, #undef
      3. #include
      4. #if
      5. #error、#warning
      6. マクロ
      7. 定義済みマクロ

9. 変数の有効範囲
      1. 変数の種類
      2. グローバル変数の有効範囲
      3. オート変数の有効範囲
      4. スタティック変数の有効範囲

10. 構造体
      1. 構造体とは
      2. 構造体の宣言
      3. 構造体の使用法
      4. 構造体のポインタ
      5. 構造体の構造体
      6. 構造体と関数
      7. 練習問題

11. 共用体
      1. 共用体とは
      2. 共用体の宣言
      3. 共用体の使用法

12. ファイル
      1. ファイル
      2. ファイル構造体
      3. ファイル作成・オープン
      4. ファイル読み込み
      5. ファイル書き込み
      6. ファイルクローズ
      7. ファイルエラー
      8. ランダムアクセス
      9. 標準入出力
      10. 練習問題

13. 低水準入出力関数
      1. 高水準入出力関数との違い
      2. ファイル作成
      3. ファイルオープン
      4. ファイル読み込み
      5. ファイル書き込み
      6. ファイルクローズ
      7. 標準入出力
      8. ランダムアクセス
      9. ファイルポインタとファイルディスクリプタ
      10. 練習問題

14. データ構造
      1. データ構造とは
      2. データ構造の種類
      3. 線形リスト
          4. 単方向リスト
          5. 双方向リスト
          6. 環状リスト
      7. ベクター
      8. 木
          9. 二分検索木
      10. スタック
      11. キュー

15 標準関数
      1. 文字分類・文字変換
      2. 文字列操作
      3. データ変換
      4. メモリー操作
      5. 数値演算
      6. ファイル操作(高水準入出力関数)
      7. ファイル操作(低水準入出力関数)
      8. プロセス関係

16 関数ポインタ
      1. 関数ポインタとは
      2. 関数ポインタ
      3. 関数ポインタと引数・戻り値
      4. 関数ポインタの配列

・ トップページに戻る


はじめに

    C++入門へようこそ。このページは、C言語を理解している人が、C++をはじめようという 人のためのページです。C言語がわからない方は先にC言語入門をお読みください。

    C言語とC++の最大の違いは、「クラス」を用いることができるかです。「クラス」とは C言語で言う「構造体」のようなものです。このメンバー変数のほかに関数を追加したものが 「クラス」です。はじめてクラスという言葉を聞いた方は「よくわからない」とか、 「便利なの？」あるいか「何のためにそんなことするの？」とお思いかもしれません。 それについては、後々解説いたします。

    このようにクラスという概念を導入したことで「オブジェクト指向言語」とも呼ばれます。 オブジェクト指向言語は他にjavaなどがあります。

    オブジェクト指向って何？　そんな声が聞こえてきそうなので、１章ではオブジェクト指向 とクラスについて簡単に触れ、その後にC++の勉強を始めることにしましょう。

← 前ページへ　:　 トップへ　:　 次ページへ →


1章：オブジェクト指向とは？

1. オブジェクト指向とクラス

    C言語入門で作成したプログラムは「手続き型」と呼ばれ、今となっては 古い作成方法です。「手続き型」では、処理の流れによってプログラムを書いていく方法です。

    「オブジェクト指向」では、処理は二の次です。一番に考えるのはオブジェクトです。 では、オブジェクトとは何かが問題になります。オブジェクトというのは、「モノ」です。 直訳なので怒られそうですが、オブジェクトとは実際に存在するもののことをいうのです。 そしてクラスとオブジェクトとは、ほぼ等価です。

    そしてこのオブジェクトには決められた仕事や処理が決まっています。これがクラスで 定義するメンバー関数です。

    ここで色鉛筆を例に取りましょう。「色鉛筆」は実際に存在するモノです。したがって オブジェクトつまり「色鉛筆クラス」になるわけです(クラスという言葉に抵抗がある方は 「色鉛筆構造体だと思ってください)。色鉛筆には様々な色がありますので、メンバー変数 として「色」を決めましょう。そして、「芯の長さ」も含めましょう。これをC言語で記述 すると、以下のようになります。

enum COLOR{
    black,
    white,
    red,
    green,
    yellow,
    blue
};

struct color_pen{
    COLOR  clr;
    int    length;
};
    ところで、色鉛筆クラスは何をするんでしょう？　当然絵を描くことができます。 それではメンバー関数を定義しましょう。これはC++独自の書き方になりますので、 C言語では書けません。
enum COLOR{
    black,
    white,
    red,
    green,
    yellow,
    blue
};

class ColorPen{
    COLOR  clr;
    int    length;

    void draw();
};
    クラスとは、こんな感じです。

    C言語では、構造体のメンバー変数とそれを処理する関数が別々に定義されていましたので、 例えば、「色鉛筆構造体」と「箸構造体」があり、「絵を描く」関数と、「食べ物を食べる」関数 があった場合、「色鉛筆で食べ物を食べる」という奇妙なプログラムも書くことができます。

    「そんなバカな」と思われるかもしれませんが、大きなプログラムを複数の人が書く場合、 「そんなバカな」というようなミスが重大なバグを生む原因であることも少なくありません。

    オブジェクト指向では、このように「モノ」と「それを扱う関数」が一緒にクラスとして 定義されていますので、「色鉛筆で食べ物を食べる」というミスは文法的に書けないことに なっています。

    整理しますと、「オブジェクト指向」では「モノ(主にデータやデバイス)を 第一優先に考える」という分析・設計・実装方法なのです。


1章：オブジェクト指向とは？

2. 継承

    先ほどは「色鉛筆」の例を紹介しました。しかし色鉛筆は鉛筆の一種であると 考えられます。鉛筆でも絵を描くことができますが、色は黒一色です。色鉛筆では 様々な色で絵を描くことができます。つまり、、、
enum COLOR{
    black,
    white,
    red,
    green,
    yellow,
    blue
};

class Pencil{
    int    length;

    void   draw();	// 黒で絵を描く
};

class ColorPen : public Pencil{
    Color  color;

    void   draw();	// colorで絵を描く
};
    このように定義すると、「色鉛筆クラスは鉛筆クラスの一種」という意味になります。 このように、既存のクラスの一種であるクラスを定義することを「継承」と言います。 色鉛筆クラスには「長さ」が定義されていませんが、鉛筆クラスで定義されていますので、 色鉛筆クラスにも「長さ」が定義されていることになります。

    この場合、色鉛筆クラスから見て鉛筆クラスを「スーパークラス」、 「基底クラス」あるいは、「親クラス」と言い、鉛筆クラスから見て 色鉛筆クラスを「サブクラス」あるいは「子クラス」と言います。

    実際にプログラムを作ると、クラスが増えていきます。複数のプログラマーが 作る場合には、どのようなクラス関係がなりたっているのかわかりにくくなりますので、 クラス関係を表すために「クラス図」と呼ばれる図を書くことをお薦めします。 この２つのクラスの関係は右図のようになります。


1章：オブジェクト指向とは？

    もう１つ例を挙げましょう。

    上のクラス図は、基底クラスとして「哺乳類クラス」、その子クラスとして、 「犬クラス」「猫クラス」そして「象クラス」を定義している。さらに「猫クラス」の 子クラスとして「三毛猫クラス」「シャム猫クラス」を定義している。

    しかし、「犬クラス」の子クラスとして「盲導犬クラス」、「警察犬クラス」を 定義することはあまりよくありません。なぜならこの場合、動物の種類として継承している ため、そこに「警察犬」「盲導犬」といった機能で継承することになるからです。

    ここはC++のページであり、オブジェクト指向のページではないので、説明を省きます。


1章：オブジェクト指向とは？

3. カプセル化

    クラスのメンバー変数、メンバー関数はだれが触っても良いものと、触るクラスを 限定したい場合もあります。誰が触っても良い物を「public」、自分と子クラスからしか さわれない物を「protected」(Javaのproctedとは機能が異なります)、そして 自分自身のクラスからしかさわれない物を「private」と言います。

    すべてのメンバー変数、メンバー関数をpublicにして、プログラマー達の決め事として 「この変数は子クラスからしか触っちゃダメね」と決めても良いのですが、中には決め事を 守らないプログラマーもいるため、public, protected, privateを決めておけば、文法的に コンパイラがチェックしてくれます。

    なぜ、そんなことをするのでしょう？　これは作ったプログラムの保守をしやすいように です。例えば、ある関数を変更しようと思ったのですが、呼び出し側に影響するかどうかを 調べるために、どこで呼び出しているかを調べたいとします。プログラムが大きくなれば すべてのファイルを探すのは面倒です。しかしその関数がprotectedであれば、自分と、 子クラスだけを調べれば良いことになります。

    基本的にメンバー変数はすべてprivateかprotectedにします。そして参照するための関数と 代入する関数を用意します。なぜこんな面倒なことをするのか、主な理由は３つあります。

    １つめはメンバー変数の型を「int」から「double」に変更しようとした場合、参照する場所と、代入 する場所の２つに影響してきます。publicな変数ですと、プログラム全てに渡って、この変数 を参照する場所と代入する場所を探すのは面倒な作業です。それに対して、関数を探すのは、 比較的容易だからです。

    ２つめは、このメンバー変数は他のクラスから参照しても良いけど、値を変更するのは ダメ、というような変数があったとします。この場合、参照する関数はpublicに、代入する 関数はprotectedかprivateにすればよいのです。

    最後に、マルチスレッドプログラムにした場合です。マルチスレッドプログラムでは、 複数のスレッドが同じ変数にアクセスする場合があります。例えば、あるスレッドがリストを ソート中に、別のスレッドがそのリストを参照した場合、矛盾が生じてしまいます。 このような場合は、「セマフォ」という、リストを参照する権利を与え、権利があるスレッド だけがリストを参照したり、いじったりできるようにします。あらかじめ参照するための関数を 用意しておけば、セマフォの処理を組み込む場合も、その関数をちょっと直すだけなので、 簡単です。

    このように、変数や関数にアクセスするできるクラスを制限することで、あるクラスを 部品のようにすることができます。部品のようになったクラスは別のプログラムを作るときにも 利用できます。例えば、Visual C++ではMFCというマイクロソフトがあらかじめ作成して くれた便利なクラスがたくさん用意されており、通信をしたいと思えばそのためのクラスを、 ファイル操作をしたければそのためのクラスを、プログラマーが使用するだけですので、 プログラムが簡単に書けます。

    また、通信をするためのクラスを利用しようと思った場合、通常その中身がどんな プログラムになっているのか、使う人(プログラマー)は知る必要がありません。つまり 通信をするためのクラスの中身はブラックボックスということになります。

    このように、中身を知らなくても部品のように使えるように設計することを 「カプセル化」と言います。


1章：オブジェクト指向とは？

4. ポリモーフィズム

    ポリモーフィズムとはギリシア語で「多数の形態」という意味です。プログラムで 「ポリモーフィズム」と言うと、同じ名前の関数を定義することを言います。

    先ほどの例に例えると、「鉛筆クラス」の「絵を描く関数」と「色鉛筆クラス」の 「絵を描く関数」は、機能は若干異なりますが、「絵を描く関数」という関数の名前は 同じです。

    また、C言語でint型の絶対値を求める関数を「abs」と名付けると、小数も扱えるような 関数を追加しようとした場合、「abs_float」と名前を変えなくてはいけませんでした。 C++では、ポリモーフィズムによって、引数が異なれば同じ名前の関数を複数定義することも できます。

    さてさて、前置きが長くなってしまったので、そろそろあくびが出てきた頃でしょうか？ いよいよ次ページからプログラムです。C言語でもそうでしたが、 新しい言語を学ぶ場合は、文法などは２番目に出てきますね。大抵は「Hello C World」の ように画面に文字を出力するプログラムを書くのが通例です。そこで、 次章では、文字の出力、入力を行います。つまり、C言語で言うprintf, scanfを 学んでもらいます。

    そしてその後に、C++の特徴の１つであるクラスについて説明していきます。


2章：ストリーム

1. 出力

    C言語で文字を表示させるためには、printfなどの関数を使用しました。C++でもprintfを 使用することはできますが、滅多に使用しません。代わりに、ostremクラスを使用します。

    ostreamクラスとは、出力用ストリームの一種で、プログラムが実行されると、標準出力 (cout)、標準エラー出力(cerr)、標準入力(cin)というものがすでに使える状態にあります。 C言語でもstdout, stderr, stdinというファイルポインタが使用できるのと同じです。

    ではさっそく、「Hello C++ World.」と表示するプログラムを紹介しましょう。

#include <iostream>

void main(){
    cout << "Hello C++ World" << endl;
}

Hello C++ World.
    ストリームを使用するためには「iostream」をインクルードする必要があります。 そして、「cout <<」の後に出力したい物を記述します。「endl」は マニピュレータと呼ばれる物の一種で、endlは改行して、 ストリームをフラッシュするという意味です。フラッシュというのは、「バッファーに 溜まっている物を出力する」という意味です。

    つまり、「標準出力のバッファーに"Hello C++ World."を溜めて、endlで、出力せよ」 という意味になります。

    「<<」の後は、文字列でなくても構いません。次の例では、int型の変数ageに 年齢を入れておき、自分の名前と年齢を表示するものです。

#include <iostream>

void main(){
    int   age  = 20;
    char* name = "Ryoko Hirose";

    cout << "My name is " << name << endl;
    cout << "I am " << age << " years old." << endl;
}

My name is Ryoko Hirosue.
I am 20 years old.
    つまり、「<<」の後が、charポインタ、intなど型を判断して、適切に表示 してくれることになります。
型 | 出力形式
char | １文字として出力
bool
int
short
long 		整数として出力
float
double 		小数として出力
char* | 文字列として出力
その他のポインタ | アドレスとして出力

    なお、ストリームと関係ありませんが、C++では「bool」という型が追加されました。 boolは主にフラグとして利用する型で、「true」か「false」が入ります。つまり1か0です。
bool flag = true;
while( flag ){
    :
    :
}
のように使用します。

    話はストリームに戻りますが、標準エラー出力に表示させたい場合は、「cout」の代わりに 「cerr」を使用するだけです。


2章：ストリーム

2. マニピュレータ

    マニピュレータは出力を書式付きで表示させるときに必要になります。 改行させたり、10進数で数字を出力したり、16進数で、あるいは8進数で表示 させるようにするなど、様々なマニピュレータが用意されています。ここでは 代表的な物をいくつか紹介します。
マニピュレータ | 解　説
endl | 改行して、フラッシュする
flush | フラッシュする
oct | 数字を表示する際に8進数で表示する
dec | 数字を表示する際に10進数で表示する
hex | 数字を表示する際に16進数で表示する
setw(int n) | 1行をn桁にします
setiosflags( ios::left ) | 左寄せで出力する
setiosflags( ios::right ) | 右寄せで出力する
resetiosflags( ios::left ) | 上述のios::leftなどを無効にする
setprecision(int n ) | 小数をn桁で表示する(デフォルトは6)

oct, dec, hex, setwの例
#include <iostream.h>
#include <iomanip.h>

void main(){
    cout << setw(5) << "dec"
         << setw(5) << "hex"
         << setw(5) << "oct"
         << endl;
    for( int i = 0 ; i < 20 ; i++ )
        cout << dec << setw(5) << i
             << hex << setw(5) << i
             << oct << setw(5) << i << endl;
}

  dec  hex  oct
    0    0    0
    1    1    1
    2    2    2
    3    3    3
    4    4    4
    5    5    5
    6    6    6
    7    7    7
    8    8   10
    9    9   11
   10    a   12
   11    b   13
   12    c   14
   13    d   15
   14    e   16
   15    f   17
   16   10   20
   17   11   21
   18   12   22
   19   13   23

setiosflags, resetiosflagsの例
#include <iostream.h>
#include <iomanip.h>

void main(){
    double values[] = { 1.23, 35.36, 653.7, 4358.24 };
    char *names[] = { "Zoot", "Jimmy", "Al", "Stan" };
    for ( int i = 0; i < 4; i++ )
       cout << setiosflags( ios::left )
            << setw( 6 )  << names[i]
            << resetiosflags( ios::left )
            << setw( 10 ) << values[i] << endl;
}

Zoot        1.23
Jimmy      35.36
Al         653.7
Stan     4358.24

setprecisionの例
#include <iostream.h>
#include <iomanip.h>

void main(){
    double values[] = { 1.23, 35.36, 653.7, 4358.24 };
    char *names[] = { "Zoot", "Jimmy", "Al", "Stan" };
    for ( int i = 0; i < 4; i++ )
       cout << setiosflags( ios::left )
            << setw( 6 )
            << names[i]
            << resetiosflags( ios::left )
            << setw( 10 )
            << setprecision( 1 )
            << values[i] << endl;
}

Zoot          1
Jimmy     4e+001
Al        7e+002
Stan      4e+003

    C言語ではブロック({...})の最初でなければなりませんでしたが、C++ではC言語と異なり、 変数の宣言がどこに記述しても良いことになりました。したがって、上述の例のように 「int i」をfor文の中に記述しても構いません。


2章：ストリーム

3. 入力

    標準入力から入力する場合は「cin」を利用します。このcinも型によって自動的に 数字を入力したり、文字列を入力したりすることができます。

型 | 入力形式
char | １文字として入力
bool | 入力不可
int
short
long 		整数として入力
float
double 		小数として入力
char* | 文字列(単語)として入力(スペースなどは削除される)
その他のポインタ | 入力不可

#include <iostream.h>

void main(){
    cout << "あなたの名字は? : " << flush;
    char last_name[512];
    cin >> last_name;

    cout << "あなたの名前は? : " << flush;
    char first_name[512];
    cin >> first_name;

    cout << "あなたの年齢は? : " << flush;
    int age;
    cin >> age;

    cout << endl;

    cout << "氏名 : " << last_name  << ' ' << first_name << endl
         << "年齢 : " << age << endl;
}

あなたの名字は? : Hirosue
あなたの名前は? : Ryoko
あなたの年齢は? : 20

氏名 : Hirosue Ryoko
年齢 : 20

    char*(char配列)の場合は、単語単位で読まれます。つまり「Hirosue Ryoko」と 入力すると、変数last_nameに「Hirosue Ryoko」と代入されるのではなく、 last_nameには「Hirosue」、first_nameには「Ryoko」と入力されることになります。

    では、１行そのまま入力したい場合はどうすれば良いのでしょうか。 「>>」を利用して１行読み込むことはできません。関数を利用します。

    cinはistremクラスのオブジェクト(インスタンス)です。したがって、istreamクラスの メンバー関数(getline)を利用します。getline関数は、引数にcharポインタと、バッファーサイズ を渡します。つまり以下のように使用します。
#include <iostream.h>

void main(){
    char line[512];
    cout << "なんか入力して" << endl;

    cin.getline( line, sizeof(line) );
    cout << line << endl;

}

なんか入力して
This is a pen.
This is a pen.

    実は、「>>」を利用して読み込んだ場合も、「getline」を利用して読み込んだ場合も 戻り値が帰ってきます。つまり以下のような使い方も可能です。
#include <iostream.h>

void main(){
    char line[512];
    cout << "なんか入力して。終了はCTRL + Zを押して" << endl;

    while(cin.getline( line, sizeof(line) ) )
        cout << line << endl;
}

なんか入力して。終了はCTRL + Zを押して
This is a pen.
This is a pen.
I have a book.
I have a book.
I love you.
I love you.


2章：ストリーム

4. ファイル

    ファイルからの入力やファイルへの出力へはそれぞれ「ifstream」クラス、「ofstream」クラス を使用します。

    fotreamクラスはostreamの子クラスで、ifstreamクラスはistreamクラスの子クラスです。 子クラスについては後述しますが、ここではofstreamはostreamつまりcoutと同じようにして 使用することができ、ifstreamクラスはistreamつまりcinと同じように使うことができると 覚えておいてください。

    ただし、coutやcinのように最初からインスタンスが存在するわけではありません。 つまり自分でファイル名を指定して、インスタンスを作らなければいけません。

    C言語ではファイルを使用する場合、fopen関数を用いてファイルをオープンしました。 ifstreamやofstreamのインスタンスを作るとは、fopen関数を用いてオープンするのと 同じ操作をすると言うことです。

    なお、ファイルストリームを使用するためには「fstream.h」をインクルードする必要が あります。

    例として、input.txtをoutput.txtにコピーするプログラムを示します。 アンダーラインの部分が、ifstream、ofstreamクラスのインスタンスを作っている部分です。 つまりファイルをオープンしている部分です。
#include <iostream.h>
#include <fstream.h>

void main(){
    char buf[512];
    ifstream ifs( "input.txt" ) ;
    ofstream ofs( "output.txt" ) ;

    while( ifs.getline( buf, sizeof(buf) ))
        ofs << buf << endl;
}


2章：ストリーム

5. 練習問題1

問題
    MS-DOSの「TYPE」コマンドを作成せよ。すなわち、引数としてファイル名を入力し、 そのファイルの内容を表示するプログラムを作れ。


2章：ストリーム

5. 練習問題1

問題
    MS-DOSの「TYPE」コマンドを作成せよ。すなわち、引数としてファイル名を入力し、 そのファイルの内容を表示するプログラムを作れ。
解答
#include <iostream.h>
#include <fstream.h>

void main( int argc, char** argv ){
    if( argc == 2 ){
        char     buf[512];
        ifstream ifs( argv[1] ) ;

        while( ifs.getline( buf, sizeof(buf) ))
            cout << buf << endl;
    }
}

C:\>MyType c:\config.sys
device=C:\WINDOWS\himem.sys
device=C:\WINDOWS\EMM386.EXE RAM
devicehigh=C:\WINDOWS\kkcfunc.sys
devicehigh=C:\WINDOWS\hrtimer.sys

dos=high,umb
lastdrive=q
C:\>


2章：ストリーム

6. 文字列

    ostreamは標準出力、標準エラー出力を、istreamは標準入力をターゲットとしていました。 ofstreamはファイル出力、ifstreamはファイル入力をターゲットにしていました。 ストリームは文字列をターゲットにすることもできます。それがostrstreamとistrstreamです。

    ostrstream, istrstreamを使用する場合は「strstrea.h」をインクルードする必要があります。 使い方はこれまでのストリームと同じです。ただしインスタンスを作成するときに、 ターゲットを指定する必要があります。

    ファイルストリームの場合は、ターゲットのファイル名を指定しましたが、ストリング ストリームでは、ターゲットとなる文字列を指定します。

    以下に、文字列を単語事に区切って出力する例を示します。
#include <iostream.h>
#include <strstrea.h>

void main(){
    char str[] = "This is a pen.";
    char buf[20] ;
    istrstream is( str ) ;
    while( is >> buf )
        cout << buf << endl;
}

This
is
a
pen.


2章：ストリーム

7. 練習問題2

問題
    ファイルから指定された単語を探し、ファイル名、行数、その行を表示するプログラムを 作れ。ただし、プログラムの第一引数として検索単語、第二引数からはファイル名(複数指定可能) とする。


2章：ストリーム

7. 練習問題2

問題
    ファイルから指定された単語を探し、ファイル名、行数、その行を表示するプログラムを 作れ。ただし、プログラムの第一引数として検索単語、第二引数からはファイル名(複数指定可能) とする。
解答
#include <string.h> // ANSI-C ランタイム、strcmp
#include <iostream.h>
#include <fstream.h>
#include <strstrea.h>

/////////////////////////////////////////////////////////////
// 定数
#define LINESIZE 1000
#define WORDSIZE 80

//////////////////////////////////////////////////////////////
// 使い方
void usage( ){
    cerr << "usage : search pattern file [ file... ]" << endl ;
}

/////////////////////////////////////////////////////////////
// サーチ関数のプロトタイプ
void search( const char* pattern, istream* p_is, const char* file ) ;

/////////////////////////////////////////////////////////////
void main( int argc, char** argv ){
    if( argc < 3 ){
        usage( ) ;
        return ;
    }

    // 検索単語
    char* pattern = argv[1] ;

    // 引数で指定されたファイルをループする
    for( int i = 2 ; i < argc ; i++ ){
        ifstream ifs( argv[i] ) ;
        search( pattern, &ifs, argv[i] ) ;
    }
}

//////////////////////////////////////////////////////////////////
// サーチ関数
void search( const char* pattern, istream* p_is, const char* file ){
    // 行番号
    int lineno = 0 ;

    // 行バッファ
    char line[LINESIZE] ;

    // 入力単語
    char word[WORDSIZE] ;

    // p_isより１行ずつ入力ストリームを読み込む
    // 作業を終端まで繰り返す
    while( p_is->getline( line, LINESIZE ) ){
        // 行番号を進める
        lineno++ ;

        // 入力行にバインドした入力文字列ストリーム(iss)を用意する
        istrstream iss( line ) ;

        // 入力文字列ストリームから順次単語を読み込む
        while( iss >> word ){
            if( strcmp( word, pattern ) == 0 )
                cout << file << " : "  << lineno << " : " << line << endl ;
        }
    }
}

C:\>search void search.cpp
stltest3.cpp : 13 : void usage( ){
stltest3.cpp : 19 : void search( const char* pattern, istream* p_is, const char* file ) ;
stltest3.cpp : 22 : void main( int argc, char** argv ){
stltest3.cpp : 40 : void search( const char* pattern, istream* p_is, const char* file ){

C:\>


3章：C++の新しい文法

1. 新しい型bool

    char, int, short, long, float, double、ポインタに加えてC++ではboolという 新しい型が用意されました。

    boolとは、「真」か「偽」すなわち「true」か「false」を表す型です。
int  n1 = 10, n2 = 20;
bool b1 = true;
bool b2 = false;
bool b3 = (n1 != n2);
bool b4 = 0;
bool b5 = 1;

while( b1 ){
    :
    :
}

for( b2 = true ; b2 ; ){
    :
    :
}

if( b3 ){
    :
    :
}
else{
    :
    :
}
のような使い方をします。

    これにより、「true」、「false」が予約語になりましたので、識別子としてこの２つの 言葉を使用することができなくなりました。

    なお、「sizeof」演算子を用いてboolのサイズを見ると１バイトの領域を必要とします。


3章：C++の新しい文法

2. デフォルト引数

    C++では関数の引数にデフォルト値を与えることができるようになりました。
#include <iostream.h>
#include <fstream.h>

void print( int n = 10 ){
    cout << n << endl;
}

void main(){
    print();
    print( 100 );
}

10
100

    上記の例のように、関数の引数に「=??」と記述します。

    関数のプロトタイプ宣言と関数の実装が別れている場合はプロトタイプ宣言にのみ記述します。
#include <iostream.h>
#include <fstream.h>

void print( int n = 10 );

void main(){
    print();
    print( 100 );
}

void print( int n );{
    cout << n << endl;
}

10
100


3章：C++の新しい文法

3. newとdelete

    C言語ではメモリーを割り当てる際にmalloc関数とfree関数を用いていた。 これらの関数はメモリーをただのブロックとして領域確保・解放を行っていた。 C++では、メモリーのブロックとしてではなく、型として領域確保する方法が追加された。 それが「new」と「delete」である。

型 C言語の場合 C++言語の場合
int型
int *p = (int*)malloc( sizeof(int) );
free(p);

int *p = new int;
delete p;
int配列
int *p = (int*)malloc( sizeof(int) * 10 );
free(p);

int *p = new int[10];
delete []p;
構造体配列
struct Test{
   int  n;
   char ch;
};

struct Test *p = (struct Test*)malloc( sizeof(struct Test) );
free(p);

struct Test{
   int  n;
   char ch;
};

Test *p = new Test;
delete p;
    上記のようにnew, deleteを利用すると簡潔に書くことができます。

    C++でもmallocを使用することはできます。しかし不便と言うだけでなく、 クラスを作成するときはnewを使用することで、 mallocを使った場合にはできないコンストラクタ、デストラクタ という機能があるからです。

    new, deleteの具体的な例は、追々出てきますので、そちらを参照してください。


3章：C++の新しい文法

4. 参照型

    参照型とは、変数・定数の別名を意味します。変数・定数の別名なので、宣言と同時に 初期化をする必要があります。普通の代入と異なり、キャストして代入することはできません。 参照の宣言は型の後に「&」をつけます。
int   i, j ;
char  c ;
int&  r1 = i ;  // r1 は 整数オブジェクト i の別名となる
int&  r2 ;      // エラー：参照型変数は初期化時に
                // 参照対象を明示しなければならない
int&  r3 = c ;  // エラー：型違反(キャストは許されない)

r1 = 10 ;       // i にも10が代入される
上の例で、iとr1は同じメモリー領域を共有するため、r1に10を代入するという動作は iにも10が代入されるという意味になります。

    参照が使われるのは、関数の引数、戻り値で、それ以外の用途は余りありません。

    では、関数の引数に参照型と参照を使わない場合の比較をしてみます。
参照 非参照
void func(int& n){
    i++;
}

void main(){
    int i = 10;
    func(i);

    cout << i << endl;
}

void func(int n){
    i++;
}

void main(){
    int i = 10;
    func(i);

    cout << i << endl;
}

11

10
    参照型の場合、func関数内の変数nはmain関数内の変数iの別名になります。 つまりこの２つの変数は同じ物になります。したがって、func関数内で、変数nの 値を変更すると、main関数内の変数iも変更されることになります。

    非参照型の場合、mainからfuncを呼ぶ際に、変数iが一時的にコピーされ、 func関数内の変数nに渡されます。func関数内で、nの値を変更しても、それは iのコピーであるために、変数iには影響しません。

    参照型は戻り値にも指定できます。
参照 非参照
int& func(){
    static inti = 10;
    return i;
}

void main(){
    cout << func()++ << endl;
    cout << func()++ << endl;
}

int func(){
    static inti = 10;
    return i;
}

void main(){
    cout << func()++ << endl;    //コンパイルエラー
    cout << func()++ << endl;    //コンパイルエラー
}

10
11
    戻り値が参照型と言うことは、func関数の戻り値と、func関数内の静的変数iが同じ物である ということです。つまり、この戻り値を「++」などで変更すれば、func関数内の静的変数iが 変更されると言うことになります。

    参照型のメリットは２つあります。
参照型のメリット
  • 関数呼び出しで無用なコピーを防ぐ
  • 関数呼び出しの連打ができる

関数呼び出しの連打とは、以下のようなことを言います。
void main(){
    cout << "abc" << 'c' << endl;
}

    後述しますが、C++の演算子は一種の関数として定義され、自分で作ることができます。 つまり、ostreamクラスの「<<」演算子は戻り値として自分自身を返します。つまり 「cout」が返ってくるために、その戻り値に対して「<< 'c'」を呼ぶことができます。 さらにその戻り値が「cout」なので、「<< endl」を行うことができるのです。

    もし、関数の引数にメンバー変数が多く定義されている構造体を渡したい場合、 構造体のメンバー変数全てがコピーされて、関数に渡されます。戻り値も同じで、 全てのメンバー変数がコピーされて呼び出し元に戻ります。もし構造体のメンバーが 1Mbytesもあれば、1MBytes分コピーされてしまいます。これは非常に効率が悪い操作に なります。これを解決するのが参照型というわけです。

    なお、参照型の例は次章で示します。


3章：C++の新しい文法

5. const

    constとは変数の前に記述し、変数ではなく定数を表すという意味の修飾子です。 つまり変更ができなくなります。また定数は初期化を行わなくてはいけません。
const int a;            // エラー：constは初期化を行わなくてはならない
const int b = 10;       // 変数bは10を表す定数
b++;                    // bは定数なので変更できない

const char* p1="abc";   // p1が指す内容を変更できない。(p1自体は変更可能)
char const* p2="abc";   // p2が指す内容を変更できない。(p2自体は変更可能)
 p1++  ;  p2++;         // ポインタ自体は変更できる
*p1='A'; *p2='A'        // エラー：ポインタが指す内容がconst

char* const p3="abc";   // p3は変更できない。(p3が指す内容は変更可能)
p3++;                   // エラー：p3の変更はできない
*p3='A';                // ポインタが指す内容は変更可能

const char* const p4="abc"; // p4、p4が指す内容ともに変更できない。
p4++;                   // エラー：p3の変更はできない
*p4='A';                // エラー：ポインタが指す内容がconst

int n = 10;
const int& r = n;       // nの参照をrに定義する。rは変更不可、nは変更可能
n++;                    // nは変更可能。rも11になる
r++;                    // エラー：constなrは変更不可能

const int& r2 = b;      // constの参照をconstにするとコンパイラー依存
bool c = ( &b == &r2 ); // cはfalseが入る場合もある(コンパイラ依存)

int& r3 = b;            // エラー：constな変数のconstではない参照を作ることはできない
    以上は、普通の変数に付けた場合の動作です。

    constには他の用例もあります。例えば、参照と組み合わせて関数の戻り値、あるいは 引数に付けます。
void func1( const int& value{
    int n = value;       // valueを変えていないので何の問題もない
    value++;             // エラー：valueは定数として認識しているので、変更不可能
}

int& func2( int& value ){
    return value;
}

const int& func3( int& value ){
    return value;
}

void main(){
    int n = 10;
    func2(n)++;          // 戻り値はconstでないので何の問題もない
    func3(n)++;          // エラー：戻り値はconstなので変更できない
}

    もう一つの用例として、メンバー変数を変更しないことを意味します。
class TestClass{
private:
    int  size;

public:
    TestClass(int n){
        size = n;
    }

    int GetSize() const{
        size++;        // エラー：メンバー変数の変更はできない
        return size;
    }
};


3章：C++の新しい文法

6. 変数の宣言

    C言語、C++言語では変数を使う際に、変数を宣言しなければいけません。 この際にC言語では{...}ブロックの最初に宣言する必要があります。これに対してC++言語では どこでも宣言してよいことになりました。ただし、スコープはそのブロック内のみです。
void main( ){
    int i = 10;
    cout << i << endl;

    int j = 100;
    cout << j << endl;
}

    さらに、「while」、「for」、「if」文のカッコの中でも変数を宣言できます。 スコープはそのブロック内のみです。
void main( ){
    if( bool b = true ){
        for( int i = 0 ; i < 10 ; i++ ){
            cout << i << endl;
        }
        cout << i << endl;        // 有効
    }
//  cout << i << endl;            // エラー：スコープ外
}


3章：C++の新しい文法

7. 例外

    C++では例外処理(exception handling)というエラー復帰機構を加えることができるようになりました。 例外を発生させたい場所に「throw」を記述し、例外が起こるかもしれないところを 「try」ブロックで囲います。例外が起きた場合に実行したいところは「catch」ブロックで 囲います。

// 例外を投げる関数
void test(int n){
    if( n == 0 )
        throw n;    // 例外を投げる部分
}

void main(){
    try{
        // 例外が起こるかもしれないブロック
        int n;
        cin >> n;
        test(n);
    }
    catch( Type1 arg ){
        // 例外1が起きた場合に処理したいブロック
    }
    catch( Type2 arg ){
        // 例外2が起きた場合に処理したいブロック
    }
    catch(...){
        // その他の例外が起きた場合に処理したいブロック
    }
}


3章：C++の新しい文法

    簡単な例として、割り算を行う関数を作ってみることにします。
#include <iostream.h>

// 割り算関数
// もしかすると例外を投げる可能性があるかもしれない
int divide(int n1, int n2){
    if( n2 == 0 )
        throw n2;    // 例外を投げる部分
    cout << "割り算を実行します" << endl;
    return n1 / n2;
}

void main(){
    int n1, n2, n3;
    cin >> n1 >> n2;

    // 例外が起こるかもしれないブロック
    try{
        n3 = divide(n1, n2);
        cout << n1 << " / " << n2 << " = " << n3 << endl;
    }
    catch( int arg ){
        // 例外が起きた場合に処理したいブロック
        cout << "余算エラー:" << arg << endl;
    }
}

C:\>exceptiontest
10 5
割り算を実行しました
10 / 5 = 2

C:\>exceptiontest
10 0
余算エラー:0

C:\>

    標準入力から数字を２つ読み込み、n1とn2に代入しています。 そして、割り算関数を呼び出すのですが、「try」ブロックで囲んでいますので、 もしかしたら例外が発生するかもしれないことを意味しています。割り算関数の中では、 第二引数が0かどうかをチェックし、0であれば例外を発生させています。

    割り算関数では例外をint型で投げていますので、main関数のcatchブロックも int型のキャッチブロックで処理されます。つまり、「余算エラー」と表示します。

    もし、型が異なり(catch(double arg)だった場合)、例外をキャッチできずに異常終了 になります。これでは例外処理機構の意味がなくなってしまうので、必ずキャッチしなければ なりません。

    割り算関数では結果を戻り値として返していますので、もし例外を使わないのであれば、 呼び出し元に割り算を実行できたかどうか通知する手段がありません。これが例外の 便利なところです。

    では、チェック自体をしなかったらどうなるでしょうか。これは実行系によって 異なります。昔のコンパイラでコンパイルし、昔のMS-DOSで実行すれば、暴走してしまい、 リセットするしかなくなります。比較的新しいMS-DOSであれば、プログラムが強制終了します。 MS-Windows系でもプログラムが強制終了され、エラーダイアログで通知されます。

    このようにプログラム内でエラーを関知して、適当に処理をし、データの保存もしていないのに プログラムが強制終了されるプログラムはダメなプログラムです。


3章：C++の新しい文法

    前ページの例では、throwする関数をtryブロックで囲って いましたが、tryブロック内でthrowしても構いません。
#include <iostream.h>

void main(){
    int n1, n2, n3;
    cin >> n1 >> n2;

    // 例外が起こるかもしれないブロック
    try{
        if( n2 == 0 )
            throw n2;
        n3 = n1 / n2;
        cout << "余算を実行しました" << endl;
        cout << n1 << " / " << n2 << " = " << n3 << endl;
    }
    catch( int arg ){
        // 例外が起きた場合に処理したいブロック
        cout << "余算エラー:" << arg << endl;
    }
}

C:\>exceptiontest
10 5
割り算を実行しました
10 / 5 = 2

C:\>exceptiontest
10 0
余算エラー:0

C:\>

    Javaを勉強したことのある人は「C++の例外処理って低機能だなぁ。」と思われるかもしれません。 確かに低機能ですが、Javaでは高機能な例外処理を実現するために、内部的にとても 複雑なことを行っています。それがJavaが低速な理由の一つです。


3章：C++の新しい文法

    次は、異なった型の複数の例外を受け取る例、それ以外の例外をすべて受けとる例を紹介します。 あくまでも例なので、プログラムに意味はありません。

#include <iostream.h>

void main(){
    int n1, n2, n3;
    cin >> n1 >> n2;

    // 例外が起こるかもしれないブロック
    try{
        if( n2 == 0 )
            throw n2;
        if( n1 == 0 )
            throw "0をどんな数で割っても0に決まってるじゃん";
        if( n1 == n2 )
            throw &n1;

        n3 = n1 / n2;
        cout << "余算を実行しました" << endl;
        cout << n1 << " / " << n2 << " = " << n3 << endl;
    }
    catch( int arg ){
        // 例外が起きた場合に処理したいブロック
        cout << "余算エラー:" << arg << endl;
    }
    catch( char* arg ){
        // 例外が起きた場合に処理したいブロック
        cout << "余算エラー:" << arg << endl;
    }
    // 上記以外の例外
    catch(...){
        cout << "int型、char*型以外の例外を受けとりました" << endl;
    }
}

C:\>exceptiontest
10 5
割り算を実行しました
10 / 5 = 2

C:\>exceptiontest
10 0
余算エラー:0

C:\>exceptiontest
0 10
余算エラー:0をどんな数で割っても0に決まってるじゃん

C:\>exceptiontest
10 10
int型、char*型以外の例外を受けとりました

C:\>

    ここでは例外の簡単な例を紹介しました。今までの例では、int型などの標準のデータ型 の例しか紹介しませんでしたが、自分で定義した構造体やクラス を投げたり受けとったりすることもできます。それについては後述します。


3章：C++の新しい文法

8. オーバーロード

    C言語では、同じ関数名の関数は１つしか作成できませんでした。つまり絶対値を求める 関数を作ろうとした場合、「abs」という名前にすると、小数に対応した絶対値を求める 関数は「abs_double」などと名前を変えて作成する必要があります。

    C++では、同じ名前の関数を作ることができるようになりましたので、このような面倒なことは しなくてもすみます。これをオーバーロードといいます。オーバーロードをするためには、 引数の数または引数の型が異なっている必要があります。戻り値は同じでも異なっていても 構いません。ただし、引数が同じで戻り値だけが異なるものはダメです。

// 絶対値を求める(整数版)
int abs(int arg){
    return (arg < 0) ? -arg : arg;
}
// 絶対値を求める(小数版)
double abs(double arg){
    return (arg < 0) ? -arg : arg;
}


3章：C++の新しい文法

9. テンプレート関数

    オーバーロードを使うと、同じ名前の関数を作ることができました。 これは便利なようで、かゆいところに手が届かない的な場合もあります。というのは、 絶対値を求める関数の場合、同じようなプログラムをint型、double型にわけて 書く必要があったからです。

    テンプレート関数とは、絶対値を求める関数のテンプレートを作っておけば、 int型、double型だけでなく、short型、long型、float型すべてに対応させることができる 優れものです。
// 絶対値を求める(テンプレート版)
template<class Type>
Type abs(Type arg){
    return (arg < 0) ? -arg : arg;
}

#include<iostream.h>

void main(){
    int n = -10;
    n = abs(n);
    cout << n << endl;

    double d = -10;
    d = abs(d);
    cout << d << endl;
}

10
10

    アンダーラインのように記述すると、その関数内で「Type」と記述したところすべてが int型、double型のように書き換わります。斜体文字「Type」を別の文字に書き換えることも できます。

    では引数が複数だった場合はどうでしょう？　それも可能です。次は、加算関数を 作ってみます。
/*
// add(int, int), add(double, double)に化けてくれるが、
// add(int, char)のように異なる型には化けてくれない。
// 異なる型にも化けさせるには、下のようなテンプレートにしなければいけない
template<class Type>
Type add(Type arg1, Type arg2){
    return arg1 + arg2;
}
*/

// 加算2(テンプレート版)
template<class Type1, class Type2>
Type1 add(Type1 arg1, Type2 arg2){
    return arg1 + arg2;
}

#include<iostream.h>

void main(){
    int n1 = 10, n2 = 20, n3;

    n3 = add(n1, n2);
    cout << n3 << endl;

    char ch = 1;
    // 加算2が呼ばれる
    n3 = add(n1, ch);
    cout << n3 << endl;
}

30
10


3章：C++の新しい文法

10. 名前空間

    C言語の標準関数をすべて覚えている人は滅多にいないと思います。さらにC++では 標準関数やクラスが増えたため、標準で定義されている識別子をすべて覚えることは 不可能に近いです。そんな中で、関数や変数を作ろうとした場合、標準で定義されている関数名と 名前がバッティングすることも少なくありません。

    現に、C++が登場した頃は、標準関数と同じ名前で、同じ引数リストを持つ関数を作って しまい、標準関数を呼びたかったのか、自分が作った関数を呼びたかったのかわからなく なったり、予期しないバグにぶち当たることも多々ありました。

    そこで考えられたのが「名前空間」です。名前空間とは、関数名などの識別子の 名前をどこで管理するかを決める機構です。

    例えば、全国に「山田太郎」さんという名前の人が10人いたとします。しかし、 「東京の山田太郎さん」と指定すれば１人かもしれません。この「東京の」にあたるのが 名前空間です。

    実際は電話番号に近いかもしれません。例えば、全国に「12-3456」という電話番号が 10件あったとします。しかし、市外局番をつけて「0468-12-3456」と指定すれば、それは 必ず１件しかありませんこの「市外局番」にあたるのが名前空間です。

    名前空間を作るためには以下のように宣言します。

namespace name{
    // 宣言
}

例）
namespace Tokyo{
    int yamada_taro;
}

    名前空間を使用した例を示します。
#include<iostream.h>

namespace Tokyo{
    const char* Yamada = "Yamada Taro";

    // TokyoのPrintName関数
    void PrintName(const char* name){
        cout << name << endl;
    }

    // TokyoのPrintName関数
    void PrintName(){
        cout << Yamada << endl;
        // Tokyoの中ではTokyo::と指定しなくてもいい
        // 同じ東京内では市外局番を省略できるのと同じ
    }
}

// グローバルなPrintName
void PrintName(const char* name){
        cout << name << "  " << name << endl;
}

void main(){
    // グローバルなPrintNameを呼ぼうとしている
    PrintName(Yamada);          // エラー：どこのYamadaさんだかわからない

    // グローバルなPrintNameを呼ぼうとしている
    PrintName(Tokyo::Yamada);   // OK：TokyoのYamadaさんを指定している

    // TokyoのPrintNameを呼ぼうとしている
    Tokyo::PrintName("Suzuki");
}

    上記の例のアンダーラインのように「::」演算子を用いて、どこの名前空間に所属しているかを 指定することで、目的の名前空間にアクセスすることができます。ただし、同じ名前空間内では 省略することができます。同じ市内では市外局番を省略できるのと同じ考えです。

    しかし、いちいち名前空間を指定するのは面倒です。例えば東京の山田太郎さんが親戚ならば、 親戚内では「山田太郎さん」と言えば、親戚の山田太郎さんであることは「東京の」と付け加えなくても わかります。

#include<iostream.h>

namespace Tokyo{
    const char* Yamada = "Yamada Taro";

    // TokyoのPrintName関数
    void PrintName(const char* name){
        cout << name << endl;
    }
}

// グローバルなPrintName
void PrintName(const char* name){
        cout << name << "  " << name << endl;
}

// これ以下は、Tokyo::と付けなくても、暗黙でTokyoを調べてくれる
using namespace Tokyo;

void main(){
    // さっきはどこのYamadaさんだかわからなかったが、今回はTokyoのYamadaさんだとわかる
    // しかし、PrintNameがTokyoのPrintNameだか、グローバルなPrintNameだかわからないので
    // エラー
    PrintName(Yamada);

    // TokyoのPrintNameを呼ぼうとしている
    // これはOK
    Tokyo::PrintName(Tokyo::Yamada);

    // グローバルなPrintNameを呼ぼうとしている
    // これもOK
    ::PrintName("Suzuki");
}

    上記のアンダーラインのように暗黙で名前空間を指定したい場合はこのように記述します。

#include<iostream.h>

namespace Tokyo{
    const char* Yamada = "Yamada Taro";

    // TokyoのPrintName関数
    void PrintName(const char* name){
        cout << name << endl;
    }
}

// グローバルなPrintName
void PrintName(const char* name){
        cout << name << "  " << name << endl;
}

// これ以下は、Tokyo::と付けなくても、暗黙でYamadaだけはTokyoを調べてくれる
using Tokyo::Yamada;

void main(){
    // 今回は、TokyoのPrintNameは見えておらず、グローバルなPrintNameのみ見えているので
    // YamadaはTokyo::を見てくれる
    // OK
    PrintName(Yamada);

    // TokyoのPrintNameを呼ぼうとしている
    // これはOK
    Tokyo::PrintName(Tokyo::Yamada);

    // グローバルなPrintNameを呼ぼうとしている
    // YamadaはTokyo::を見てくれる
    // これもOK
    ::PrintName(Yamada);
}

Yamada Taro  Yamada Taro
Yamada Taro
Yamada Taro  Yamada Taro

    ある特定の変数や関数だけを暗黙で指定したい場合は、上記のアンダーラインのように 記述します。

    名前空間に含めることができるのは、変数や関数だけではありません。クラス宣言自体を 名前空間に納めることもできます。

    C++の標準ライブラリは「std」という名前空間に納められています。


4章：クラス

1. クラスとは

    クラスとは、構造体にそのデータを扱うための関数をあわせたものであることは 説明しました。二章で紹介したifsetreamクラスや、ofstream クラスのデータには、ファイル名やファイルディスクリプタなどが含まれています。 そしてそれらを扱う関数として、getlineなどの関数が含まれています。

    このように、データとそれらを扱う関数を作成したクラスは、そのクラスの内部が どのようになっているのかを知らなくても、部品のように使用することができます。 つまり、ifstreamやofstreamの中身を知らなくても、サンプルプログラムで作成した ように、そのクラスを使って、プログラムを作ることができます。さらに、その部品 (クラス)は他のプログラムを作成するときにも使い回しができるのです。

    なお、クラスのデータのことを「メンバー変数」、そのデータを扱う 関数を「メンバー関数」と言い、両者を併せて「メンバー」と言います。


4章：クラス

2. クラスの宣言

    クラスの宣言を行う際には、どのメンバーを公開し、どのメンバーは公開しないかを 決めます。すなわち、ある変数や関数は、自分のクラス内からしかアクセスできないようにするか、 どの変数・関数はクラス外からアクセスしても良いようにするかです。

    例えば、ofstreamクラスでは、getline関数のようにmainから呼んでも良いような物を 公開しても良い関数と言い、ファイルディスクリプタのように、mainから触ることのできない 変数を公開しないと言います。

    公開しても良いものを「public(公開)」と言います。mainのようにクラス外から アクセスできないメンバーを「private(私有)」または、「protected(保護)」 と言います。

    privateなメンバーは自分のクラスからしかアクセスできません。protectedなメンバーは 自分のクラスとその子クラス(後述)からしかアクセスできません。また、公開されていない 関数のことを「ヘルパー関数」と呼ぶこともあります。

    基本的に、メンバー変数はすべてprotectedかprivateにします。そしてその変数を 取得したい場合は「get_???」のような関数を用意し、その関数を経由してメンバー変数を 取得するようにします。これはカプセル化のところで説明しました。

    このようにどのメンバーを公開し、どのメンバーを非公開にするかを決めたらいよいよ 宣言をします。宣言は以下のようにします。
class TestClass{
public:
    void PublicFunc1();
    int  PublicFunc2(int);
protected:
    void ProtectedFunc1();
    char ProtectedFunc2();
private:
    void  PrivateFunc1();
    char* PrivateFunc2();

public:
    char ch;
protected:
    char* filename;
private:
    int  size;
};
    赤字の関数・変数はpublic、青字の関数・変数はprotected、緑字の関数・変数はprivateに なります。


4章：クラス

3. クラスの実装

    一般に１つのクラスは、１つのヘッダーファイルと１つのCPPファイルから構成されます。 前ページのようなクラス宣言はヘッダーファイルに記述し、実際のプログラムはCPPファイルに 記述します。ヘッダファイルやCPPファイルのファイル名は、クラスと同じ名前を付けるのが 一般的です。
TestClass.h
class TestClass{
public:
    int   getSize();
    char* getFileName();

protected:
    int   size;
    char* filename;
};
TestClass.cpp
#include "TestClass.h"

int TestClass::getSize(){
    return size;
}

int TestClass:getFileName(){
    return filename;
}
    上記のように「戻り値　クラス名::関数名」というふうに実装します。この「::」を 「スコープ演算子」と言います。

    また、下記のようにクラス宣言の中で実装することもできます。
TestClass.h
class TestClass{
public:
    int getSize(){
        return size;
    }

    char* getFileName(){
        return filename;
    }

protected:
    int   size;
    char* filename;
};


4章：クラス

4. コンストラクタとデストラクタ

    クラスには、インスタンスが作られた時に１度だけ実行される「コンストラクタ」と、 インスタンスが削除される時に１度だけ実行される「デストラクタ」が、特別な 関数として定義できる。

    一般にコンストラクタにはそのクラスインスタンスを初期化するために使用され、 デストラクタは後かたづけ処理を行う。

    前述のファイルストリームでは、ファイルをオープンしたり クローズしたりする関数を呼び出さなくても良かった。これはコンストラクタでファイルを オープンし、デストラクタでファイルをクローズしていたからである。

    コンストラクタの関数名はクラス名と同じ名前で、引数をとることはできるが、 戻り値はない。デストラクタの関数名はクラス名の前に「~」を付けたもので、引数も 戻り値もない。
TestClass.h
class TestClass{
public:
    TestClass();    // ←コンストラクタ
    TestClass(int)  // ←引き数付きコンストラクタ
    ~TestClass();   // ←デストラクタ
    int   getSize();
    char* getFileName();

protected:
    int   size;
    char* filename;
};


4章：クラス

5. クラスの使用法

    クラスを使用するためにはまずインスタンスを作らなければならない。構造体のインスタンス と同様に作成する方法と「new演算子」を使用する方法の２種類がある。

    前者の方法では、スコープから抜ければ自動的にクラスのインスタンスも削除される。 しかし「new演算子」を用いて作成したインスタンスは、スコープを抜けても削除されない。 削除したい場合は、「delete演算子」を使用しなければならない。newしたものをdelete し忘れると、OSによってはプログラムが終了してもメモリーにずっと残ってしまう場合も あるので、newしたものは必ずdeleteしなければならない。
TestClass.h
class TestClass{
public:
    TestClass(int n);    // コンストラクタ
    ~TestClass();        // デストラクタ
    void   print();

protected:
    int   size;
};
TestClass.cpp
#include "TestClass.h"
#include<iostream.h>

// コンストラクタ
TestClass::TestClass(int n){
    cout << "コンストラクタ" << n;
    size = n;
}

// デストラクタ
TestClass::~TestClass(){
    cout << "デストラクタ" << size;
}

void TestClass::print(){
    cout << "print" << size;
}
main.cpp
#include "TestClass.h"

void main(){
    TestClass  test(10);
    test.print();      // 10と表示される

    TestClass* pTest = new TestClass(20);
    pTest->print();    // 20と表示される
    delete pTest;
}

コンストラクタ10
print10
コンストラクタ20
print20
デストラクタ20
デストラクタ10


4章：クラス

6. 例題）スタッククラス

    例題として、逆ポーランド電卓を作ります。

    逆ポーランド電卓とは、「1 2 +」は「3」、「15 10 -」は「5」、「15 10 - 3 *」は「15」 というような感じで、数字が入力されたらスタックにプッシュし、+のような演算子が来たら スタックから数字を２つポップして計算結果をスタックにプッシュするような計算機です。

    逆ポーランド電卓を作るためにはスタックを作る必要があります。

    コンストラクタにスタックサイズを指定します。デフォルトは100です。デフォルトでは int 100個分の配列をnewします。デストラクタではこのnewした配列を削除します。 また、pushで値をこの配列に格納します。popでは、pushした値を配列から取り出して 返します。get_lengthでは現在のスタックに溜められている個数を返します。peekでは 指定位置(スタックトップからのオフセット)のスタック要素を覗き見るようにします。

    メンバー変数は、配列を格納するint型ポインタと、現在のスタック位置を示す スタックポインタを持ちます。これら変数はprivateです。

    以上をクラスにすると以下のようになるはずです。
Stack.h
/////////////////////////////////////////////////////////////////////////////
//
//    整数のスタッククラス(動的メモリ割当版)定義
//

// ２重取り込みを防止する
#ifndef INTSTACK_H
#define INTSTACK_H

#include<iostream.h>

//////////////////////////////////////////////////////////////////////////////
// スタッククラス

class IntStack {

public:

    // コンストラクタ：
    // IntStack オブジェクトが定義された時に自動的に呼び出される
    IntStack( int sz = 100 ){
        // コンストラクタ引数を省略すると、スタックサイズは100となる
        // スタックサイズを保存する
        size = sz ;

        // スタックの実体(size個のint配列)を自由記憶上に割り当てる
        stack = new int[size] ;

        // スタックポインタを初期化する
        sp = 0 ;
    }

    // デストラクタ：
    // IntStack オブジェクトが削除される時に自動的に呼び出される
    ~IntStack(){
        //スタック本体を削除する
        delete [ ] stack ;
    }

    // プッシュ
    void push( int value ){
        // オーバーフローのチェック
        if( sp >= size ){
            cerr << "stack overflow" << endl ;
            exit( 1 ) ;
        }

        *( stack + sp ) = value ;
        sp++ ;
    }

    // ポップ
    void pop( int* p_value ){
        // アンダーフローのチェック
        if( sp <= 0 ){
            cerr << "stack underflow" << endl;
            exit( 1 ) ;
        }

        sp--;
        *p_value = *( stack + sp );
    }

    // 現在のスタック長を獲得する
    int  get_length(){
            return sp;
    }

    // 指定位置(スタックトップからのオフセット)のスタック要素を覗き見る
    void peek( int* p_value, int offset = 0 ){
        // オフセットのチェック
        if( offset >= sp ){
            cerr << "stack underflow" << endl ;
            exit( 1 ) ;
        }

        // 指定位置の要素をコピーする
        *p_value = *( stack + sp - offset - 1 );
    }

private:

    ////////////////////////////////////////////////////
    // 管理情報

    // スタックポインタ：次にプッシュする位置
    int sp;

    // スタックサイズ
    int size;

    // スタック
    int *stack;
};

#endif // INTSTACK_H
main.cpp
#include"Stack.h"

////////////////////////////////////////////////////////////////////////////
// メイン

int main( int argc, char** argv ){
    int value, operand1, operand2 ;

    // スタックテンプレートクラスから、実数スタックを具体化する
    IntStack ostack( argc - 1 );

    // コマンド行引数からリバーシュポーリッシュ記法で記述された式を
    // 読み込み計算する
    for( int i = 1 ; i < argc ; i++){
        // 加法
        if( strcmp( argv[i], "+" ) == 0 ){
            ostack.pop( &operand2 );
            ostack.pop( &operand1 );
            ostack.push( operand1 + operand2 );
        }

        // 減法
        else if( strcmp( argv[i], "-" ) == 0 ){
            ostack.pop( &operand2 );
            ostack.pop( &operand1 );
            ostack.push( operand1 - operand2 );
        }

        // 乗法
        else if( strcmp( argv[i], "*" ) == 0 ){
            ostack.pop( &operand2 );
            ostack.pop( &operand1 );
            ostack.push( operand1 * operand2 );
        }

        // 除法
        else if( strcmp( argv[i], "/" ) == 0 ){
            ostack.pop( &operand2 );
            ostack.pop( &operand1 );

            // 0除算のチェック
            if( operand2 == 0 ){
                cerr << "divid by 0" << endl;
                return 1;
            }

            ostack.push( operand1 / operand2 );
        }

        // 上記以外は整数を仮定する
        else{
            value = atoi( argv[i] );
            ostack.push( value );
        }
    }

    // スタックの整合性の確認
    if( ostack.get_length( ) != 1 ){
        cerr << "illegal expression" << endl;
        return 1 ;
    }

    // 答を出力する
    ostack.pop( &value );
    cout << value << endl;

    // 正常終了
    return 0;
        // ostack がスコープから抜ける時、自動的にデストラクタが
        // 呼び出される
}

C:\>clac 15 10 - 3 *
15

C:\>


4章：クラス

7. テンプレートクラス

    前ページのスタッククラスはint型専用のスタッククラスで、 逆ポーランド電卓も整数のみでした。ここでは整数のみでなく、小数も扱えるようにします。

    IntStackをDoubleStackと改名して、int型の部分をdouble型に変更すれば良いのですが、 これでは、文字列を扱う場合は文字列型スタッククラスを、他の型にする場合は、専用の スタッククラスを作成しなければいけなくなります。これは同じようなプログラムを何個も 作成しなければいけないことを意味します。

    それでは面倒なので、ここではテンプレートクラスにします。テンプレートクラスとは、 今までint型と書いていたものを、何でも良い型にするということです。つまり１つスタッククラス を作れば、そのスタッククラスがint型にも、char型にも、ポインタ型にも、クラス型にも 化けてくれるクラスです。

    テンプレートクラスは以下のように定義します。
template <class 仮型名>  class  クラス名  { クラス定義本体 } ;

定義例(TestClass.h)

template<class Type>
class TestClass
{
public:
    Type hensu;

    Type GetHensu(){
        return hensu;
    }

    void SetHensu(Type value);
};

template<class Type>
void TestClass<Type>::SetHensu( Type value ){
    hensu = value;
}

クラス作成

TestClass<int>    intClass;      // ←int  型のTestClassを作成
TestClass<char>   charClass;     // ←char 型のTestClassを作成
TestClass<char*>  pcharClass;    // ←char*型のTestClassを作成

    上記の例では、intClass内の「hensu」変数はint型に、charClass内の「hensu」変数は char型になります。

    テンプレートクラスのメンバー関数の実装は、CPPファイルに記述してはいけません。 クラス宣言外に関数を実装する場合(上で言うSetHensu関数)も必ずヘッダーファイル内に 記述する必要があります。

    では、小数にも対応したスタッククラスと、その使用例として、小数型の逆ポーランド電卓 の例を示します。アンダーラインの部分が変更した点です。(クラス名の変更は除く)
Stack.h
/////////////////////////////////////////////////////////////////////////////
//
//    テンプレートスタッククラス(動的メモリ割当版)定義
//

// ２重取り込みを防止する
#ifndef STACK_H
#define STACK_H

#include<iostream.h>

//////////////////////////////////////////////////////////////////////////////
// スタッククラス

template<class Type>
class Stack {

public:

    // コンストラクタ：
    // Stack オブジェクトが定義された時に自動的に呼び出される
    Stack( int sz = 100 ){
        // コンストラクタ引数を省略すると、スタックサイズは100となる
        // スタックサイズを保存する
        size = sz ;

        // スタックの実体(size個のType配列)を自由記憶上に割り当てる
        stack = new Type[size] ;

        // スタックポインタを初期化する
        sp = 0 ;
    }

    // デストラクタ：
    // Stack オブジェクトが削除される時に自動的に呼び出される
    ~Stack(){
        //スタック本体を削除する
        delete [ ] stack ;
    }

    // プッシュ
    void push( Type value ){
        // オーバーフローのチェック
        if( sp >= size ){
            cerr << "stack overflow" << endl ;
            exit( 1 ) ;
        }

        *( stack + sp ) = value ;
        sp++ ;
    }

    // ポップ
    void pop( Type* p_value ){
        // アンダーフローのチェック
        if( sp <= 0 ){
            cerr << "stack underflow" << endl;
            exit( 1 ) ;
        }

        sp--;
        *p_value = *( stack + sp );
    }

    // 現在のスタック長を獲得する
    int  get_length(){
            return sp;
    }

    // 指定位置(スタックトップからのオフセット)のスタック要素を覗き見る
    void peek( Type* p_value, int offset = 0 ){
        // オフセットのチェック
        if( offset >= sp ){
            cerr << "stack underflow" << endl ;
            exit( 1 ) ;
        }

        // 指定位置の要素をコピーする
        *p_value = *( stack + sp - offset - 1 );
    }

private:

    ////////////////////////////////////////////////////
    // 管理情報

    // スタックポインタ：次にプッシュする位置
    int sp;

    // スタックサイズ
    int size;

    // スタック
    Type *stack;
};

#endif // STACK_H
main.cpp
#include"Stack.h"

////////////////////////////////////////////////////////////////////////////
// メイン

int main( int argc, char** argv ){
    double value, operand1, operand2 ;

    // スタックテンプレートクラスから、実数スタックを具体化する
    Stack<double> ostack( argc - 1 );

    // コマンド行引数からリバーシュポーリッシュ記法で記述された式を
    // 読み込み計算する
    for( int i = 1 ; i < argc ; i++){
        // 加法
        if( strcmp( argv[i], "+" ) == 0 ){
            ostack.pop( &operand2 );
            ostack.pop( &operand1 );
            ostack.push( operand1 + operand2 );
        }

        // 減法
        else if( strcmp( argv[i], "-" ) == 0 ){
            ostack.pop( &operand2 );
            ostack.pop( &operand1 );
            ostack.push( operand1 - operand2 );
        }

        // 乗法
        else if( strcmp( argv[i], "*" ) == 0 ){
            ostack.pop( &operand2 );
            ostack.pop( &operand1 );
            ostack.push( operand1 * operand2 );
        }

        // 除法
        else if( strcmp( argv[i], "/" ) == 0 ){
            ostack.pop( &operand2 );
            ostack.pop( &operand1 );

            // 0除算のチェック
            if( operand2 == 0 ){
                cerr << "divid by 0" << endl;
                return 1;
            }

            ostack.push( operand1 / operand2 );
        }

        // 上記以外は整数を仮定する
        else{
            value = atof( argv[i] );
            ostack.push( value );
        }
    }

    // スタックの整合性の確認
    if( ostack.get_length( ) != 1 ){
        cerr << "illegal expression" << endl;
        return 1 ;
    }

    // 答を出力する
    ostack.pop( &value );
    cout << value << endl;

    // 正常終了
    return 0;
        // ostack がスコープから抜ける時、自動的にデストラクタが
        // 呼び出される
}

C:\>clac2 15.2 11.2 - 2.5 *
10

C:\>

    スタッククラス内のプライベート変数「sp」は、int型ですが、これは配列の何番目を 示すかの変数で、スタックがdoubleになろうが、char*になろうが、配列のインデックスは 整数なので、int型のままになることに注意してください。


4章：クラス

8. 練習問題

問題
    標準入力から１行ずつ読み込み、逆順にして表示するプログラムを作成する。 ただし、Stackクラスは前ページのテンプレートスタック クラスを使用し、char*型のスタッククラスのインスタンスを作る。


4章：クラス

8. 練習問題

問題
    標準入力から１行ずつ読み込み、逆順にして表示するプログラムを作成する。 ただし、Stackクラスは前ページのテンプレートスタック クラスを使用し、char*型のスタッククラスのインスタンスを作る。
解答
////////////////////////////////////////////////////////////////////////////
//
//    入力行を逆順に出力する - スタックテンプレートクラスを利用する
//

#include <string.h>
#include <iostream.h>
#include "Stack.h"

// 定数定義
#define LINE_SIZE 100
#define MAX_LINES 1000
//////////////////////////////////////////////////////////////////////////
// メイン

int main( ){
    // 入力行スタック ： MAX_LINES 迄の行を保存できる
    Stack<char*> lines( MAX_LINES );

    // 行バッファ
    char line[LINE_SIZE];

    char* p;

    // 標準入力ストリームから行を順次読み込む
    while( cin.getline( line, LINE_SIZE ) ){
        // 入力行を複製する
        p = new char[strlen(line)+1];
        strcpy( p, line );

        // 入力行のアドレスをスタックに積む
        lines.push( p );
    }

    // スタックの先頭から順次文字列を取り出し、出力する
    while( lines.get_length( ) > 0 ){
        // 文字列をスタックの先頭から取り出し出力する
        lines.pop( &p );
        cout << p << '\n';

        // 不要となった文字列を解放する
        delete [ ] p;
    }

    // 正常終了
    return 0;
}

C:\>revline
This
is
a
pen.
^Z
pen.
a
is
This
C:\>


4章：クラス

9. 参照型

    参照型は、関数呼び出しの際に引数や戻り値の無用なコピーを防ぐための機能で、 前述したとおりです。

    スタッククラスのpushは参照を使用していないため、引数がpush関数を呼び出す際に １回、そして、配列にコピーする際に１回、合計２回コピーをしているために効率が悪い。 そこで、push関数の引数を参照型にする。

    しかし参照型にするとpush関数の中で変更されるという危険性が生じる。もちろん、 スタッククラスを作ったプログラマーは、関数の中で変更していないことを知っているが、 クラスにするということは、中身を知らない人でも使えるようにする必要があるという ことである。つまり、push関数の中では引数として渡す参照型の値が変更されないことを 示してやる必要がある。これが「const」修飾子である。

    つまり、スタッククラスのpush関数の宣言は以下のようになる。
template<class Type>
class Stack{
public:
    void push( const Type& value );
};

    以下はスタッククラスと逆ポーランド電卓です。変更点はアンダーラインで示した、 スタッククラスのpush関数のみになります。
Stack.h
/////////////////////////////////////////////////////////////////////////////
//
//    テンプレートスタッククラス(動的メモリ割当版)定義
//

// ２重取り込みを防止する
#ifndef STACK_H
#define STACK_H

#include<iostream.h>

//////////////////////////////////////////////////////////////////////////////
// スタッククラス

template<class Type>
class Stack {

public:

    // コンストラクタ：
    // Stack オブジェクトが定義された時に自動的に呼び出される
    Stack( int sz = 100 ){
        // コンストラクタ引数を省略すると、スタックサイズは100となる
        // スタックサイズを保存する
        size = sz ;

        // スタックの実体(size個のType配列)を自由記憶上に割り当てる
        stack = new Type[size] ;

        // スタックポインタを初期化する
        sp = 0 ;
    }

    // デストラクタ：
    // Stack オブジェクトが削除される時に自動的に呼び出される
    ~Stack(){
        //スタック本体を削除する
        delete [ ] stack ;
    }

    // プッシュ
    void push( const Type& value ){
        // オーバーフローのチェック
        if( sp >= size ){
            cerr << "stack overflow" << endl ;
            exit( 1 ) ;
        }

        *( stack + sp ) = value ;
        sp++ ;
    }

    // ポップ
    void pop( Type* p_value ){
        // アンダーフローのチェック
        if( sp <= 0 ){
            cerr << "stack underflow" << endl;
            exit( 1 ) ;
        }

        sp--;
        *p_value = *( stack + sp );
    }

    // 現在のスタック長を獲得する
    int  get_length(){
            return sp;
    }

    // 指定位置(スタックトップからのオフセット)のスタック要素を覗き見る
    void peek( Type* p_value, int offset = 0 ){
        // オフセットのチェック
        if( offset >= sp ){
            cerr << "stack underflow" << endl ;
            exit( 1 ) ;
        }

        // 指定位置の要素をコピーする
        *p_value = *( stack + sp - offset - 1 );
    }

private:

    ////////////////////////////////////////////////////
    // 管理情報

    // スタックポインタ：次にプッシュする位置
    int sp;

    // スタックサイズ
    int size;

    // スタック
    Type *stack;
};

#endif // STACK_H
main.cpp
#include"Stack.h"

////////////////////////////////////////////////////////////////////////////
// メイン

int main( int argc, char** argv ){
    double value, operand1, operand2 ;

    // スタックテンプレートクラスから、実数スタックを具体化する
    Stack<double> ostack( argc - 1 );

    // コマンド行引数からリバーシュポーリッシュ記法で記述された式を
    // 読み込み計算する
    for( int i = 1 ; i < argc ; i++){
        // 加法
        if( strcmp( argv[i], "+" ) == 0 ){
            ostack.pop( &operand2 );
            ostack.pop( &operand1 );
            ostack.push( operand1 + operand2 );
        }

        // 減法
        else if( strcmp( argv[i], "-" ) == 0 ){
            ostack.pop( &operand2 );
            ostack.pop( &operand1 );
            ostack.push( operand1 - operand2 );
        }

        // 乗法
        else if( strcmp( argv[i], "*" ) == 0 ){
            ostack.pop( &operand2 );
            ostack.pop( &operand1 );
            ostack.push( operand1 * operand2 );
        }

        // 除法
        else if( strcmp( argv[i], "/" ) == 0 ){
            ostack.pop( &operand2 );
            ostack.pop( &operand1 );

            // 0除算のチェック
            if( operand2 == 0 ){
                cerr << "divid by 0" << endl;
                return 1;
            }

            ostack.push( operand1 / operand2 );
        }

        // 上記以外は整数を仮定する
        else{
            value = atof( argv[i] );
            ostack.push( value );
        }
    }

    // スタックの整合性の確認
    if( ostack.get_length( ) != 1 ){
        cerr << "illegal expression" << endl;
        return 1 ;
    }

    // 答を出力する
    ostack.pop( &value );
    cout << value << endl;

    // 正常終了
    return 0;
        // ostack がスコープから抜ける時、自動的にデストラクタが
        // 呼び出される
}

C:\>clac2 15.2 11.2 - 2.5 *
10

C:\>


4章：クラス

10. 代入演算子

    代入演算子とは「=」のことである。C言語の構造体で以下のように代入演算子を用いると、 構造体のメンバーが全てコピーされた。
struct meibo{
    char name[20];
    int  age;
};

struct meibo abe;
struct meibo tmp;

abe.name = "Abe Natsumi";
abe.age  = 19;

tmp = abe;

    クラスも同様に代入演算子を使用すると、メンバー変数が全てコピーされます。 「メンバー変数を全部コピーする」という代入演算子はコンパイラが自動的に作成してくれます。 実はこのことは便利なようで危険がいっぱいです。

    スタッククラスを例にとって説明します。スタッククラスではコンストラクタで、 引数で渡された領域をnewしています。つまりメンバー変数はサイズと、割り当てた メモリーが格納されています。不用意にアドレスがコピーされ、一方のインスタンスが 削除されればデストラクタで割り当てたメモリーが解放されてしまいます。つまり コピーした方のスタックのpushなど関数を呼べば、すでに解放されたメモリーを アクセスすることになり、実行時エラーになってしまいます。

void main(){
    stack<int> stack1(10);
    // スタッククラスのメンバー変数stackにint型配列10個分が確保される
    // 例えば、1000番地に割り当てられたとすると、stack1.stack = 1000になる
    {
        stack<int> stack2(10);
        // スタッククラスのメンバー変数stackにint型配列10個分が確保される
        // 例えば、2000番地に割り当てられたとすると、stack1.stack = 2000になる

        stack2 = stack1;
        // 自動的にメンバー変数がコピーされる。つまり、
        // stack2.stack = 1000になる
        // 2000番地に割り当てられたメモリーは迷子になり、解放することができなくなる。
    }
    // stack2のインスタンスが削除される。つまりデストラクタが呼ばれる
    // デストラクタ内で、1000番地のメモリーは解放されてしまう。

    stack1.push(10);
    // stack1.stack = 1000番地はすでに解放されてしまっている。
    // この時点で開放された領域にアクセスしようとするので、エラーが出てしまう。
}
// stack1のインスタンスが削除される。つまりデストラクタが呼ばれる
// デストラクタ内で、すでに解放されている1000番地のメモリーは解放しようとし、エラー。

    このように、メンバー変数として、ポインタを持ち、且つデストラクタでdeleteする ようなクラスは勝手に代入されることを防がなければなりません。

    どのように防げばよいのでしょう。C++では、演算子を作ることもできます。つまり コンパイラに頼らずに自分で代入演算子を作ってしまえばよいのです。なお、演算子の 作り方は後述します。ここではスタッククラスの代入演算子の例を示します。変更点は アンダーラインの部分です。
Stack.h
/////////////////////////////////////////////////////////////////////////////
//
//    テンプレートスタッククラス(動的メモリ割当版)定義
//

// ２重取り込みを防止する
#ifndef STACK_H
#define STACK_H

#include<iostream.h>

//////////////////////////////////////////////////////////////////////////////
// スタッククラス

template<class Type>
class Stack {

public:

    // コンストラクタ：
    // Stack オブジェクトが定義された時に自動的に呼び出される
    Stack( int sz = 100 ){
        // コンストラクタ引数を省略すると、スタックサイズは100となる
        // スタックサイズを保存する
        size = sz ;

        // スタックの実体(size個のType配列)を自由記憶上に割り当てる
        stack = new Type[size] ;

        // スタックポインタを初期化する
        sp = 0 ;
    }

    // デストラクタ：
    // Stack オブジェクトが削除される時に自動的に呼び出される
    ~Stack(){
        //スタック本体を削除する
        delete [ ] stack ;
    }

    // プッシュ
    void push( const Type& value ){
        // オーバーフローのチェック
        if( sp >= size ){
            cerr << "stack overflow" << endl ;
            exit( 1 ) ;
        }

        *( stack + sp ) = value ;
        sp++ ;
    }

    // ポップ
    void pop( Type* p_value ){
        // アンダーフローのチェック
        if( sp <= 0 ){
            cerr << "stack underflow" << endl;
            exit( 1 ) ;
        }

        sp--;
        *p_value = *( stack + sp );
    }

    // 現在のスタック長を獲得する
    int  get_length(){
            return sp;
    }

    // 指定位置(スタックトップからのオフセット)のスタック要素を覗き見る
    void peek( Type* p_value, int offset = 0 ){
        // オフセットのチェック
        if( offset >= sp ){
            cerr << "stack underflow" << endl ;
            exit( 1 ) ;
        }

        // 指定位置の要素をコピーする
        *p_value = *( stack + sp - offset - 1 );
    }

    // 代入演算子
    Stack<Type>& operator=( const Stack<Type>& src ){
        // 自己代入でなければ
        if( &src != this ){
            // 現在自分が保有しているスタックを削除する
            delete [] stack;

            // サイズをコピーする
            size  = src.size;

            // スタックポインタをコピーする
            sp    = src.sp;

            // サイズ分の配列を作る
            stack = new Type[size];

            // 配列の中身をコピーする
            for( int i = 0 ; i < size ; i++ )
                stack[i] = src.stack[i];
        }
        return *this;
    }

private:

    ////////////////////////////////////////////////////
    // 管理情報

    // スタックポインタ：次にプッシュする位置
    int sp;

    // スタックサイズ
    int size;

    // スタック
    Type *stack;
};

#endif // STACK_H

    代入演算子では、現在の配列をdeleteして、コピー元の配列と同じ大きさの配列を 作る。そしてサイズと、配列の要素全てをコピーする。もし、コピー元とコピー先が 同じ場合(a=aのような場合)、これを自己代入という。自己代入の場合、自分の配列 (自己代入なので、コピー元の配列と同じ)を削除し、コピーもとの配列をコピーしようと すると、コピー元の配列はすで削除されているため、コピーできない。したがって、 自己代入を行おうとしているのかをチェックし、自己代入ならば何もしないと言う 操作をする必要がある。このため、if文で自己代入かどうかのチェックを行う必要がある。

    なお、その他の演算子については後述する。


4章：クラス

11. コピーコンストラクタ

    コピーコンストラクタとは引数に自分と同じクラスを持つコンストラクタである。 つまり、スタッククラスの場合、コンストラクタにスタッククラスを引数にとるコン ストラクタを言う。

    コピーコンストラクタも自分で定義しない場合は、コンパイラが自動的に作成する。 どのようなコピーコンストラクタが作成されるかというと、代入演算子と同じで、 メンバー変数を全てコピーするという原始的なコンストラクタである。

    つまり代入演算子と同様、メンバー変数にポインタを持ち、デストラクタでその ポインタが指すメモリー領域をdeleteするようなクラスを作成する場合は、コピーコンストラクタ も自分で定義する必要がある。

    コピーコンストラクタを作る前に、どのような場合にコピーコンストラクタが呼ばれるかを 考えてみる。
// Stack を引数とする関数
void func1( Stack<int> stack ){
    :
    :
}

// Stack の参照を引数とする関数
void func2( Stack<int> stack ){
    :
    :
}

// Stack を戻り値とする関数
Stack<int> func3( ){
    Stack<int> stack = 10;         // Stack(int)の呼び出し
    return stack;                  // コピーコンストラクタの呼び出し
                                   // 自動変数stackを呼び出し元のテンポラリ領域にコピーする
}

// テストアプリ
void main(){
    Stack<int>  stack1;            // デフォルトコンストラクタの呼び出し
    Stack<int>  stack2(10);        // Stack(int)の呼び出し
    Stack<int>  stack3 = 20;       // Stack(int)の呼び出し
    Stack<int>  stack4 = stack1;   // コピーコンストラクタの呼び出し

    stack3 = stack2;               // 代入演算子(=)の呼び出し
    func1( stack2 );               // コピーコンストラクタの呼びだし
                                   // 実引数stack2をfunc1の仮引数にコピーする
    func2( stack2 );               // 参照なのでコピーコンストラクタは呼ばれない
    Stack<int>  stack5 = func3();
           // func3の戻り値はテンポラリ領域にコピーされた後、
           // stack5 にコピーされる。つまり、コピーコンストラクタ
           // は２回呼び出される。コンパイラによっては、このコピー
           // 操作を１回で済ませるよう最適化する場合もある
}

    コピーコンストラクタは以下のように定義します。
class TestClass{
    TestClass( const TestClass& src ){
        :
        :
    }
};

    コピーコンストラクタの引数は、constな参照型でなければなりません。もし、参照型 でなければ、コピーコンストラクタを呼び出すときに、コピー元がコピーされて渡される ことになります。つまりコピーコンストラクタを呼び出すためにはコピーコンストラクタを 呼ばなければいけないと言う、奇妙な現象が起きてしまうからです。したがって、コピー コンストラクタは引数がコピーされないよう、参照型にする必要があります。また、 コピーコンストラクタがコピー元を触らないことを保証するため、「const」を付けます。

    自分でコピーコンストラクタを作成したスタッククラスは以下のようになります。 変更点はアンダーラインの部分です。
Stack.h
/////////////////////////////////////////////////////////////////////////////
//
//    テンプレートスタッククラス(動的メモリ割当版)定義
//

// ２重取り込みを防止する
#ifndef STACK_H
#define STACK_H

#include<iostream.h>

//////////////////////////////////////////////////////////////////////////////
// スタッククラス

template<class Type>
class Stack {

public:

    // コンストラクタ：
    // Stack オブジェクトが定義された時に自動的に呼び出される
    Stack( int sz = 100 ){
        // コンストラクタ引数を省略すると、スタックサイズは100となる
        // スタックサイズを保存する
        size = sz ;

        // スタックの実体(size個のType配列)を自由記憶上に割り当てる
        stack = new Type[size] ;

        // スタックポインタを初期化する
        sp = 0 ;
    }

    // コピーコンストラクタ
    Stack( const Stack<Type>& src ){
        copy( src ) ;
    }

    // デストラクタ：
    // Stack オブジェクトが削除される時に自動的に呼び出される
    ~Stack(){
        //スタック本体を削除する
        delete [ ] stack ;
    }

    // プッシュ
    void push( const Type& value ){
        // オーバーフローのチェック
        if( sp >= size ){
            cerr << "stack overflow" << endl ;
            exit( 1 ) ;
        }

        *( stack + sp ) = value ;
        sp++ ;
    }

    // ポップ
    void pop( Type* p_value ){
        // アンダーフローのチェック
        if( sp <= 0 ){
            cerr << "stack underflow" << endl;
            exit( 1 ) ;
        }

        sp--;
        *p_value = *( stack + sp );
    }

    // 現在のスタック長を獲得する
    int  get_length(){
            return sp;
    }

    // 指定位置(スタックトップからのオフセット)のスタック要素を覗き見る
    void peek( Type* p_value, int offset = 0 ){
        // オフセットのチェック
        if( offset >= sp ){
            cerr << "stack underflow" << endl ;
            exit( 1 ) ;
        }

        // 指定位置の要素をコピーする
        *p_value = *( stack + sp - offset - 1 );
    }

    // 代入演算子
    Stack<Type>& operator=( const Stack<Type>& src ){
        // 自己代入でなければ
        if( &src != this ){
            // 現在自分が保有しているスタックを削除する
            delete [] stack;

            // コピー元の配列をコピーする
            copy( src );
        }
        return *this;
    }

private:
    // 代入演算子とコピーコンストラクタから呼ばれる
    // コピーもとのメンバー変数size、spをコピーし、
    // 新しい配列を割り当て、配列の中身をコピーする
    void copy( const Stack<Type>& src ){
        // サイズをコピーする
        size  = src.size;

        // スタックポインタをコピーする
        sp    = src.sp;

        // サイズ分の配列を作る
        stack = new Type[size];

        // 配列の中身をコピーする
        for( int i = 0 ; i < size ; i++ )
            stack[i] = src.stack[i];
    }

    ////////////////////////////////////////////////////
    // 管理情報

    // スタックポインタ：次にプッシュする位置
    int sp;

    // スタックサイズ
    int size;

    // スタック
    Type *stack;
};

#endif // STACK_H

    コピー操作は、代入演算子とコピーコンストラクタの両方から呼ばれるため、 ヘルパー関数として、privateなcopy関数を用意し、これを呼ぶようにした。


4章：クラス

12. 構造体

    クラスは構造体にメンバー関数を組み込んだものですと説明しました。実は、C++では 構造体もメンバー関数を持つことができるように拡張されました。しかもコンストラクタ、 デストラクタさえ持つことができます。

    ではクラスと構造体何が違うのでしょうか？　違いはほとんどありません。デフォルトが クラスではprivateなのに対して、構造体ではpublicであるというだけです。
クラス 構造体
class TestClass{
    //デフォルトはプライベート
    int privateInt;

public:
    int publicInt;
}

struct TestStruct{
    //デフォルトはパブリック
    int publicInt;

public:
    int publicInt2;
}

    試しに、スタッククラスをクラスから構造体に変更してみてください。「class」を 「struct」に変更するだけです。コンパイルはもちろん、実行も正常にできると思います。


4章：クラス

13. メンバー変数の初期化

    C言語では構造体を宣言する際に、メンバー変数を初期化することができました。
struct meibo{
    char name[20];
    char tel[15];
    char address[50];
};

meibo abe = { "abe natsuni", "012-3456-7890", "hokkaido uncharakanchara 1-2-3" };
    C++では、構造体、クラスともに、すべてのメンバー変数がpublicな場合に限って 上述のように、メンバー変数の初期化が可能です。


4章：クラス

14. 内部クラス

    内部クラスとは、あるクラスの中で定義されたクラスのことを言います。
class OuterClass{
    class InnerClass{
    public:
        int menber;
    };

    int        menber;
    InnerClass in;

    OuterClass( int n ){
        menber = n;
    }
};
    内部クラスだけでなく、内部構造体、内部共用体も作ることもできます。

    内部クラスは、あるクラスの内部でのみ使用するデータとして使われることが多いです。 以下の例は、マップ(辞書)クラスの例です。マップでは、キーと呼ばれるデータ検索の 元となるデータと、バリューというデータ本体を持ちます、マップに登録する際には キーとバリューを格納します。検索する際には、キーを引数にとり、バリューを返します。 つまり、Mapクラス内でのみ使用するデータとして、キーとバリューからなるデータクラスを 作成します。

    アプリケーションとしては、英和辞書です。したがってキーが英語、バリューは その日本語訳になります。文字列を使用するので、未完成ではありますが、簡単な stringクラスを一緒に作ってみました。

map.h
///////////////////////////////////////////////////////////////////////////
//
//    テンプレートマップクラス定義
//

// ２重取り込みを防止する
#ifndef MAP_H
#define MAP_H
#include <iostream.h>

//////////////////////////////////////////////////////////////////////////////
// マップクラス

template<class Key, class Value>
// 内部クラス
class Map {
    class Data{
    public:
        Key   key;
        Value value;
    };

public:

    // コンストラクタ
    Map( int sz = 100 ){
        size  = 0;
        mapdata = new Data[sz] ;
    }

    // コピーコンストラクタ
    Map( const Map<Key, Value>& src ){
        copy( src ) ;
    }

    // デストラクタ
    // Stack オブジェクトが削除される時に自動的に呼び出される
    ~Map(){
        //スタック本体を削除する
        delete [ ] mapdata ;
    }

    // データの追加
    void add( const Key& k, const Value& v ){
        mapdata[size].key   = k;
        mapdata[size].value = v;
        size++;
    }

    // データの検索
    bool get( const Key& k, Value* v ){
        for( int i = 0 ; i < size ; i++ ){
            if( mapdata[i].key == k ){
                *v = mapdata[i].value;
                return true;
            }
        }
        return false;
    }

    // 現在の項目数を返す
    int get_size(){
        return size;
    }

    // 代入演算子
    Map<Key, Value>& operator=( const Map<Key, Value>& src ){
        // 自己代入でなければ
        if( &src != this ){
            // 現在自分が保有しているスタックを削除する
            delete [] mapdata;

            // コピー元の配列をコピーする
            copy( src );
        }
        return *this;
    }

private:
    // 代入演算子とコピーコンストラクタから呼ばれる
    // コピーもとのメンバー変数size、spをコピーし、
    // 新しい配列を割り当て、配列の中身をコピーする
    void copy( const Map<Key, Value>& src ){
        // サイズをコピーする
        size  = src.size;

        // サイズ分の配列を作る
        mapdata = new Data[size];

        // 配列の中身をコピーする
        for( int i = 0 ; i < size ; i++ )
            mapdata[i] = src.mapdata[i];
    }

    ////////////////////////////////////////////////////
    // 管理情報

    // 要素数
    int size;

    // データ
    Data *mapdata;
};

#endif // MAP_H

string.h

///////////////////////////////////////////////////////////////////////////
//
//    文字列クラス定義
//

// ２重取り込みを防止する
#ifndef STRING_H
#define STRING_H

#include<string.h>
#include <iostream.h>
//////////////////////////////////////////////////////////////////////////////
// 文字列クラス
class string{
    char* p;

    void copy( const char* src ){
        int len = strlen(src);
        pQ = new char[len+1];
        strcpy(p, src);
    }
public:
    // コンストラクタ
    string(const char* src){
        copy(src);
    }

    // コピーコンストラクタ
    string( const string& src){
        copy(src.p);
    }

    // デフォルトコンストラクタ
    string(){
        p = new char[1];
        *p = '\0';
    }

    // デストラクタ
    ~string(){
        delete p;
    }

    // 比較演算子
    bool operator==( const string& a )const{
        return strcmp( p, a.p ) == 0;
    }

    // 代入演算子
    string& operator=( const string& src ){
        if( &src != this ){
            delete p;
            copy(src.p);
        }
        return *this;
    }

    // ストリーム出力
    friend ostream& operator<<( ostream& os, const string& s );

};

// ストリーム出力
ostream& operator<<( ostream& os, const string& s ){
    os << s.p;
    return os;
}
#endif // STRING_H

main.cpp

#include <iostream.h>
#include <process.h>
#include "map.h"
#include "string.h"

void main(){
    Map<string, string> map(6);
    map.add( "white",  "白" );
    map.add( "black",  "黒" );
    map.add( "red",    "赤" );
    map.add( "yellow", "黄色" );
    map.add( "green",  "緑" );
    map.add( "blue",   "青" );

    char buffer[512];

    while( cin >> buffer ){
        string eng = buffer;
        string jap;
        if( map.get( eng, &jap ) )
            cout << eng  << "\t" << jap << endl;
        else
            cout << eng << "\t" << "未登録です" << endl;
    }
}

C:\>maptest
red
red     赤
white
white   白
yellow
yellow  黄色
pink
pink    未登録です
^Z

C:\>

    上記の例にはまだ解説していない「friend」と 演算子の作り方がでてきていますが、これらについては後述します。


4章：クラス

    内部クラスは、前ページの例のように、privateあるいは protectedにして外部からは見えないようにし、そのクラスの中でのみ使用するのが 一般的です。

    しかし下記の例のように、publicで内部クラスを宣言すると、外部(例えばmain関数)から 内部クラスのインスタンスをつくったり、外部クラスから内部クラスをさわることもできます。

class OuterClass{
public:
    class InnerClass{
    public:
        int a;
    };

    int        b;
    InnerClass in;
};

void main(){
    OuterClass::InnerClass in;       // 内部クラスのインスタンスを外部から作る
    OuterClass             out;

    in.a = 100;
    out.in.a = 10;                   // 外部クラスのインスタンスを利用して
                                     // 内部クラスを操作
}


4章：クラス

15. 無名クラス

    無名クラスとは、内部クラスの一種ですが、名前のないクラスのことを言います。
class OuterClass{
public:
    class{
    public:
        int num1;
        int num2;
        int num3;

        // だれも呼べないので無意味
        void func3( int n ){
            num3 = n;
        }
    };

    void func2( int n ){
        num2 = n;

        func3(n);      // エラー
    }
};

void main(){
    OuterClass out;

    out.num1 = 10;
    out.func2(20);
    out.func3(30);     // エラー
}

    無名クラスは、メンバー関数は定義しません。定義できますが、たとえpublicであっても その関数を呼び出すことができないからです。また、名前がないため、コンストラクタや デストラクタも作れません。

    さらに、無名クラスの場合、無名クラスのメンバーは全てpublicにします。そうしないと外部 クラスを含めてどこからもアクセスできなくなります。publicなメンバー変数は外部クラスや外部 (例えばmain関数)からアクセスできます。

    このように無名クラスは多くの制限があるため、外部クラスのみで使用する単純なデータ構造として 使用されます。

    同じような構造体として、無名構造体も定義できます。


4章：クラス

16. 無名共用体

    無名クラス・無名構造体ははっきり言って、ほとんど使われません。 この無名共用体の方が、まだ用途があるでしょう。

    無名共用体とは、外部クラスのメンバー変数のうち、同じメモリー領域を共有しても よい変数をまとめてしまうものです。

#include<iostream.h>

class Number{
public:
    // 整数か小数かの区別
    enum KIND{
        INTEGER,
        DOUBLE,
    }kind;

    // 実際のデータ
    union{
        int    Int;
        double Double;
    };

    // 小数をもらうコンストラクタ
    Number(double d){
        *this = d;
    }

    // 整数をもらうコンストラクタ
    // デフォルトコンストラクタを含む
    Number(int n = 0){
        *this = n;
    }

/*
    // 今回の場合は、コンパイラが自動的に用意してくれるコピーコンストラクタでも問題ない
    Number( const Number& src ){
        kind = src.kind;
        if( kind == Number::INTEGER )
            Int = src.Int;
        else if( kind == Number::DOUBLE )
            Double = src.Double;
        return *this;
    }
*/

    // 代入演算子：整数をセットする
    Number& operator=(const int& n){
        Int = n;
        kind = INTEGER;
        return *this;
    }

    // 代入演算子：小数をセットする
    Number& operator=(const double& d){
        Double = d;
        kind = DOUBLE;
        return *this;
    }

/*
    // 今回の場合は、コンパイラが自動的に用意してくれるコピー演算子でも問題ない
    // コピー演算子
    Number& operator=(const Number& src){
        kind = src.kind;
        if( kind == Number::INTEGER )
            Int = src.Int;
        else if( kind == Number::DOUBLE )
            Double = src.Double;
        return *this;
    }
*/
};

// 標準出力に表示
ostream& operator<<( ostream& os, const Number& a ){
    // 整数なら整数を出力
    if( a.kind == Number::INTEGER )
        os << a.Int;

    // 小数なら小数を出力
    else if( a.kind == Number::DOUBLE )
        os << a.Double;

    return os;
}

void main(){
    Number num = 10;
    cout << num << endl;

    num= 10.4;
    cout << num << endl;
}

10
10.4

    上記の例では、IntとDoubleという２つの変数を共用体として、同じ領域に確保しています。 無名共用体なので、普通のクラスのメンバー変数のように扱うことができます。


4章：クラス

17. 演算子の作り方

    ストリームの使い方で、「cout << "abc";」と書けば、画面に 文字列が出力されることを述べましたが、なぜ「<<」なのでしょうか。もともと、 「<<」演算子は、シフト演算子と呼ばれ、ビット単位で左にシフトする演算子でした。 これは、ostreamクラスで、シフト演算子を書き換えて、独自の「<<」演算子を 定義しているからです。

    つまり、クラスを作るときに、そのクラス専用の演算子を定義できることを示しています。 特に代入演算子については前述しました。

    演算子は大きく分けて単項演算子と２項演算子の２種類に分けることができます。

単項演算子
! ~ *
++ -- -
new delete (cast)

２項演算子
+ - * / %
< <= == > >= !=
&& || & | ^
<< >> [] ->
= += -= *= /= %=
<<= >>= &= |= ^=

    演算子も関数の一種であると考えられます。単項演算子は引数を伴わない関数であり、 ２項演算子は、引数を１つとる関数であると考えられます。

    前ページでは、「Number」クラスを作りました。 この中で、代入演算子「=」を定義しました。これは以下のように使用します。
void main(){
    Number num;
    num = 10;    // operator=(int n)が呼ばれる
}

    上記の表を見ると代入演算子は２項演算子です。つまりint型の10という引数を伴う 「operator=」という関数であるということです。実際に次のように記述しても 同様に機能します。
void main(){
    Number num;
    num.operator=(10);    // operator=(int n)が呼ばれる
}

    この場合、「num」のことを「起動オブジェクト」と言います。

#include<iostream.h>

class Number{
public:
    // 整数か小数かの区別
    enum KIND{
        INTEGER,
        DOUBLE,
    }kind;

    // 実際のデータ
    union{
        int    Int;
        double Double;
    };

    // 小数をもらうコンストラクタ
    Number(double d){
        *this = d;
    }

    // 整数をもらうコンストラクタ
    // デフォルトコンストラクタを含む
    Number(int n = 0){
        *this = n;
    }

    // 代入演算子：整数をセットする
    Number& operator=(const int& n){
        Int = n;
        kind = INTEGER;
        return *this;
    }

    // 代入演算子：小数をセットする
    Number& operator=(const double& d){
        Double = d;
        kind = DOUBLE;
        return *this;
    }

    // 足し算：小数を足す
    double operator+(const double& d)const{
        return (kind == DOUBLE ? Double + d : Int + d);
    }

    // 足し算：整数を足す
    double operator+(const int& n)const{
        return (kind == DOUBLE ? Double + n : Int + n);
    }
};

    このように定義した場合は、以下の(1)のような使い方はできますが、(2)のような使い方は できません。
void main(){
    Number num = 10;  // int型を伴うコンストラクタの呼び出し
    num = num + 10;   // (1) operator+(int n)とoperator=(double)が呼ばれる
    num = 10 + num;   // (2) エラー
}

    (2)がエラーになるのは、起動オブジェクトがnumではなく、int型の10になってしまうからです。 このような使い方をしたい場合には、クラスのメンバー関数として定義するのではなく、グローバル 関数として、+演算子を定義しなければなりません。

double operator+(const int& n, const Number& num){
    return num + n;
}
double operator+(const double& d, const Number& num){
    return num + d;
}

void main(){
    Number num = 10;  // int型を伴うコンストラクタの呼び出し
    num = num + 10;   // operator+(int n)とoperator=(double)が呼ばれる
    num = 10 + num;   // グローバルなoperator+(int n)constが呼ばれる
}

    演算子の引数は参照を使うのが一般的のようです。しかし、constな定数を渡されても使用 できるよう、上述のようにconstな参照を引数にします。これは無用なコピーを防ぐようにする ためです。


4章：クラス

    では、次に単項演算子の例を紹介します。単項演算子は、メンバー関数として定義します。 グローバルな関数として定義することはできません。

    ここではキャスト演算子を作成します。前述のNumberクラスを、int型、double型にキャスト する演算子を作成します。
#include<iostream.h>

class Number{
public:
    // 整数か小数かの区別
    enum KIND{
        INTEGER,
        DOUBLE,
    }kind;

    // 実際のデータ
    union{
        int    Int;
        double Double;
    };

    // 小数をもらうコンストラクタ
    Number(double d){
        *this = d;
    }

    // 整数をもらうコンストラクタ
    // デフォルトコンストラクタを含む
    Number(int n = 0){
        *this = n;
    }

    // 代入演算子：整数をセットする
    Number& operator=(const int& n){
        Int = n;
        kind = INTEGER;
        return *this;
    }

    // 代入演算子：小数をセットする
    Number& operator=(const double& d){
        Double = d;
        kind = DOUBLE;
        return *this;
    }

    // 足し算：小数
    double operator+(const double& d)const{
        return (kind == DOUBLE ? Double + d : Int + d);
    }

    // 足し算：整数
    double operator+(const int& n)const{
        return (kind == DOUBLE ? Double + n : Int + n);
    }

    // int型へのキャスト
    operator int()const{
        return (kind == DOUBLE ? (int)Double : (int)Int);
    }

    // double型へのキャスト
    operator double()const{
        return (kind == DOUBLE ? (double)Double : (double)Int);
    }
};

double operator+(const int& n, const Number& num){
    return num + n;
}
double operator+(const double& d, const Number& num){
    return num + d;
}

void main(){
    Number num = 10.4;
    cout << (double)num << endl;
    cout << (int)num << endl;
}

10.4
10

    +=などの代入四則演算子や <などの比較演算子を実装したNumberクラスは以下のように なります。
Number.h

#ifndef NUMBER_H
#define NUMBER_H

class Number{
public:
    // 整数か小数かの区別
    enum KIND{
        INTEGER,
        DOUBLE,
    }kind;

    // 実際のデータ
    union{
        int    Int;
        double Double;
    };

    // 小数をもらうコンストラクタ
    Number(double d){
        *this = d;
    }

    // 整数をもらうコンストラクタ
    // デフォルトコンストラクタを含む
    Number(int n = 0){
        *this = n;
    }

    // 代入演算子：整数をセットする
    Number& operator=(const int& n){
        Int = n;
        kind = INTEGER;
        return *this;
    }

    // 代入演算子：小数をセットする
    Number& operator=(const double& d){
        Double = d;
        kind = DOUBLE;
        return *this;
    }

    // 足し算：小数
    double operator+(const double& d)const{
        return (kind == DOUBLE ? Double + d : Int + d);
    }

    // 足し算：整数
    double operator+(const int& n)const{
        return (kind == DOUBLE ? Double + n : Int + n);
    }

    // 足し算：小数
    Number& operator+=(const double& d){
        if( kind == DOUBLE )
            Double += d;
        else{
            Double = (double)Int + d;
            kind = DOUBLE;
        }
        return *this;
    }

    // 足し算：整数
    double operator+=(const int& n){
        if( kind == DOUBLE )
            Double += n;
        else
            Int += n;
        return *this;
    }

    // 引き算：小数
    double operator-(const double& d)const{
        return (kind == DOUBLE ? Double - d : Int - d);
    }

    // 引き算：整数
    double operator-(const int& n)const{
        return (kind == DOUBLE ? Double - n : Int - n);
    }

    // 引き算：小数
    Number& operator-=(const double& d){
        if( kind == DOUBLE )
            Double -= d;
        else{
            Double = (double)Int - d;
            kind = DOUBLE;
        }
        return *this;
    }

    // 引き算：整数
    double operator-=(const int& n){
        if( kind == DOUBLE )
            Double -= n;
        else
            Int -= n;
        return *this;
    }

    // かけ算：小数
    double operator*(const double& d)const{
        return (kind == DOUBLE ? Double * d : Int * d);
    }

    // かけ算：整数
    double operator*(const int& n)const{
        return (kind == DOUBLE ? Double * n : Int * n);
    }

    // かけ算：小数
    Number& operator*=(const double& d){
        if( kind == DOUBLE )
            Double *= d;
        else{
            Double = (double)Int * d;
            kind = DOUBLE;
        }
        return *this;
    }

    // かけ算：整数
    double operator*=(const int& n){
        if( kind == DOUBLE )
            Double *= n;
        else
            Int *= n;
        return *this;
    }

    // 除算：小数
    double operator/(const double& d)const{
        return (kind == DOUBLE ? Double / d : Int / d);
    }

    // 除算：整数
    double operator/(const int& n)const{
        return (kind == DOUBLE ? Double / n : Int / n);
    }

    // 除算：小数
    Number& operator/=(const double& d){
        if( kind == DOUBLE )
            Double /= d;
        else{
            Double = (double)Int / d;
            kind = DOUBLE;
        }
        return *this;
    }

    // 除算：整数
    double operator/=(const int& n){
        if( kind == DOUBLE )
            Double /= n;
        else
            Int /= n;
        return *this;
    }

    // int型へのキャスト
    operator int()const{
        return (kind == DOUBLE ? (int)Double : (int)Int);
    }

    // double型へのキャスト
    operator double()const{
        return (kind == DOUBLE ? (double)Double : (double)Int);
    }

    // 比較演算子
    operator <(const int& n)const{
        return ( kind == DOUBLE ? Double < n : Int < n);
    }
    // 比較演算子
    operator <=(const int& n)const{
        return ( kind == DOUBLE ? Double <= n : Int <= n);
    }
    // 比較演算子
    operator >(const int& n)const{
        return ( kind == DOUBLE ? Double > n : Int > n);
    }
    // 比較演算子
    operator >=(const int& n)const{
        return ( kind == DOUBLE ? Double >= n : Int >= n);
    }
    // 比較演算子
    operator ==(const int& n)const{
        return ( kind == DOUBLE ? Double == n : Int == n);
    }
    // 比較演算子
    operator !=(const int& n)const{
        return ( kind == DOUBLE ? Double != n : Int != n);
    }
};

// 足し算：整数
double operator+(const int& n, const Number& num){
    return num + n;
}

// 足し算：小数
double operator+(const double& d, const Number& num){
    return num + d;
}

// 引き算：整数
double operator-(const int& n, const Number& num){
    return (num.kind == num.DOUBLE ? n - num.Double : n - num.Int);
}

// 引き算：小数
double operator-(const double& d, const Number& num){
    return (num.kind == num.DOUBLE ? d - num.Double : d - num.Int);
}

// かけ算：整数
double operator*(const int& n, const Number& num){
    return num * n;
}

// かけ算：小数
double operator*(const double& d, const Number& num){
    return num * d;
}

// 除算：整数
double operator/(const int& n, const Number& num){
    return (num.kind == num.DOUBLE ? num.Double / n : num.Int / n);
}

// 除算：小数
double operator/(const double& d, const Number& num){
    return (num.kind == num.DOUBLE ? num.Double / d : num.Int / d);
}

// 比較演算子
operator <(const int& n, const Number& num){
    return num > n;
}
// 比較演算子
operator <=(const int& n, const Number& num){
    return num >= n;
}
// 比較演算子
operator >(const int& n, const Number& num){
    return num < n;
}
// 比較演算子
operator >=(const int& n, const Number& num){
    return num <= n;
}
// 比較演算子
operator !=(const int& n, const Number& num){
    return num != n;
}
// 比較演算子
operator ==(const int& n, const Number& num){
    return num == n;
}
#endif //NUMBER_H


4章：クラス

    では、最後に「new」演算子と「delete」演算子を作ってみます。以下の例は、 new演算子とdelete演算子をオーバーライドして、ログを取るようにしたものです。

#include <iostream.h>
#include <stdlib.h>

int fLogMemory = 0;        // Perform logging (0=no; nonzero=yes)?
int cBlocksAllocated = 0;  // Count of blocks allocated.

// User-defined operator new.
void *operator new( unsigned int  stAllocateBlock ){
    static fInOpNew = 0;    // Guard flag.

    if( fLogMemory && !fInOpNew ){
        fInOpNew = 1;
        cout << "Memory block "   << ++cBlocksAllocated
             << " allocated for " << stAllocateBlock
             << " bytes\n";
        fInOpNew = 0;
    }

    return malloc( stAllocateBlock );
}

// User-defined operator delete.
void operator delete( void *pvMem ){
    static fInOpDelete = 0;    // Guard flag.
    if( fLogMemory && !fInOpDelete ){
        fInOpDelete = 1;
        cout << "Memory block " << --cBlocksAllocated
             << " deallocated\n";
        fInOpDelete = 0;
    }

    free( pvMem );
}

void main(){
    fLogMemory = 1;  // Turn logging on.
    for( int i = 0; i < 5 ; i++ ){
        char *pMem = new char[10];
        delete[] pMem;
        delete new char;
    }
    fLogMemory = 0;
    return;
}

Memory block 1 allocated for 10 bytes
Memory block 0 deallocated
Memory block 1 allocated for 1 bytes
Memory block 0 deallocated
Memory block 1 allocated for 10 bytes
Memory block 0 deallocated
Memory block 1 allocated for 1 bytes
Memory block 0 deallocated
Memory block 1 allocated for 10 bytes
Memory block 0 deallocated
Memory block 1 allocated for 1 bytes
Memory block 0 deallocated
Memory block 1 allocated for 10 bytes
Memory block 0 deallocated
Memory block 1 allocated for 1 bytes
Memory block 0 deallocated
Memory block 1 allocated for 10 bytes
Memory block 0 deallocated
Memory block 1 allocated for 1 bytes
Memory block 0 deallocated


4章：クラス

18. friend

    変数は基本的に全てをprivateかprotectedにするということは前述の通りです。 しかし特別な関数からは無条件にprivateあるいはprotectedの変数や関数にアクセスしたい 場合があります。このときにfriendを用いると、他のクラスや外部の関数から privateやprotectedの変数・関数にアクセスすることができます。

    「特別な場合」というのは、演算子を作る場合や、GUIなどです。例えば、 比較演算子(==, <, >, <=, >=)は、クラスの中身を調べる必要があることが ほとんどです。この場合、いちいち変数を取得する関数を呼び出しているのは 面倒なので、friendとして定義して、その演算子からは無条件で変数にアクセスできる ようにします。

    前述のNumberクラスでは、「double operator-(const int& n, const Number& num)」という 外部関数から実際のデータを表す「Int」「Double」という変数にアクセスしています。これは 「Int」「Double」という変数がpublicな変数であるため、実装できていますが、本来ならば 「Int」「Double」はprivateかprotectedにするべきです。もし、privateかprotectedに してしまえば、外部関数からこれら変数にアクセスできなくなってしまいます。 したがって、friendにして、外部関数からでもアクセスできるようにします。

    また、GUIはメンバー変数を表示する場合が多々あります。この場合も変数を取得する 関数を呼び出すのは面倒なので、GUIを担うクラスをfriendとしておきます。

    このようにfriendには２つの使い方があります。1)外部関数をfriendにする場合と、 2)他のクラスをfriendにする場合です。

    あるクラスをfriendクラスにするためには、クラス宣言内で、「friend class ???」 と宣言します。またある関数をfriendにするためには、同じくクラス宣言内で、 「friend 戻り値 関数名(引数の型...)」と宣言します。

    では、前述のNumberクラスの変数「Int」「Double」をprivateにしてみます。これに 伴って、４つの外部関数がfriend関数になります。また、GUIクラスをfriendにしてみます。 変更点はアンダーラインで示しました。

Number.h

#ifndef NUMBER_H
#define NUMBER_H

class Number{
friend class GUI;    // GUIクラスをfriendにする
public:
    // 整数か小数かの区別
    enum KIND{
        INTEGER,
        DOUBLE,
    }kind;

private:
    // 実際のデータ
    union{
        int    Int;
        double Double;
    };

public:
    // 小数をもらうコンストラクタ
    Number(double d){
        *this = d;
    }

    // 整数をもらうコンストラクタ
    // デフォルトコンストラクタを含む
    Number(int n = 0){
        *this = n;
    }

    // 代入演算子：整数をセットする
    Number& operator=(const int& n){
        Int = n;
        kind = INTEGER;
        return *this;
    }

    // 代入演算子：小数をセットする
    Number& operator=(const double& d){
        Double = d;
        kind = DOUBLE;
        return *this;
    }

    // 足し算：小数
    double operator+(const double& d)const{
        return (kind == DOUBLE ? Double + d : Int + d);
    }

    // 足し算：整数
    double operator+(const int& n)const{
        return (kind == DOUBLE ? Double + n : Int + n);
    }

    // 足し算：小数
    Number& operator+=(const double& d){
        if( kind == DOUBLE )
            Double += d;
        else{
            Double = (double)Int + d;
            kind = DOUBLE;
        }
        return *this;
    }

    // 足し算：整数
    double operator+=(const int& n){
        if( kind == DOUBLE )
            Double += n;
        else
            Int += n;
        return *this;
    }

    // 引き算：小数
    double operator-(const double& d)const{
        return (kind == DOUBLE ? Double - d : Int - d);
    }

    // 引き算：整数
    double operator-(const int& n)const{
        return (kind == DOUBLE ? Double - n : Int - n);
    }

    // 引き算：小数
    Number& operator-=(const double& d){
        if( kind == DOUBLE )
            Double -= d;
        else{
            Double = (double)Int - d;
            kind = DOUBLE;
        }
        return *this;
    }

    // 引き算：整数
    double operator-=(const int& n){
        if( kind == DOUBLE )
            Double -= n;
        else
            Int -= n;
        return *this;
    }

    // かけ算：小数
    double operator*(const double& d)const{
        return (kind == DOUBLE ? Double * d : Int * d);
    }

    // かけ算：整数
    double operator*(const int& n)const{
        return (kind == DOUBLE ? Double * n : Int * n);
    }

    // かけ算：小数
    Number& operator*=(const double& d){
        if( kind == DOUBLE )
            Double *= d;
        else{
            Double = (double)Int * d;
            kind = DOUBLE;
        }
        return *this;
    }

    // かけ算：整数
    double operator*=(const int& n){
        if( kind == DOUBLE )
            Double *= n;
        else
            Int *= n;
        return *this;
    }

    // 除算：小数
    double operator/(const double& d)const{
        return (kind == DOUBLE ? Double / d : Int / d);
    }

    // 除算：整数
    double operator/(const int& n)const{
        return (kind == DOUBLE ? Double / n : Int / n);
    }

    // 除算：小数
    Number& operator/=(const double& d){
        if( kind == DOUBLE )
            Double /= d;
        else{
            Double = (double)Int / d;
            kind = DOUBLE;
        }
        return *this;
    }

    // 除算：整数
    double operator/=(const int& n){
        if( kind == DOUBLE )
            Double /= n;
        else
            Int /= n;
        return *this;
    }

    // int型へのキャスト
    operator int()const{
        return (kind == DOUBLE ? (int)Double : (int)Int);
    }

    // double型へのキャスト
    operator double()const{
        return (kind == DOUBLE ? (double)Double : (double)Int);
    }

    // 比較演算子
    operator <(const int& n)const{
        return ( kind == DOUBLE ? Double < n : Int < n);
    }
    // 比較演算子
    operator <=(const int& n)const{
        return ( kind == DOUBLE ? Double <= n : Int <= n);
    }
    // 比較演算子
    operator >(const int& n)const{
        return ( kind == DOUBLE ? Double > n : Int > n);
    }
    // 比較演算子
    operator >=(const int& n)const{
        return ( kind == DOUBLE ? Double >= n : Int >= n);
    }
    // 比較演算子
    operator ==(const int& n)const{
        return ( kind == DOUBLE ? Double == n : Int == n);
    }
    // 比較演算子
    operator !=(const int& n)const{
        return ( kind == DOUBLE ? Double != n : Int != n);
    }

    // 以下の引き算をfriend関数にする
    friend double operator-(const int& n, const Number& num);
    friend double operator-(const double& d, const Number& num);

    // クラス宣言の中で実装することもできる
    friend double operator/(const int& n, const Number& num){
        return (num.kind == num.DOUBLE ? num.Double / n : num.Int / n);
    }

    friend double operator/(const double& d, const Number& num){
        return (num.kind == num.DOUBLE ? num.Double / d : num.Int / d);
    }
};

// 足し算：整数
double operator+(const int& n, const Number& num){
    return num + n;
}

// 足し算：小数
double operator+(const double& d, const Number& num){
    return num + d;
}

// 引き算：整数
double operator-(const int& n, const Number& num){
    return (num.kind == num.DOUBLE ? n - num.Double : n - num.Int);
}

// 引き算：小数
double operator-(const double& d, const Number& num){
    return (num.kind == num.DOUBLE ? d - num.Double : d - num.Int);
}

// かけ算：整数
double operator*(const int& n, const Number& num){
    return num * n;
}

// かけ算：小数
double operator*(const double& d, const Number& num){
    return num * d;
}

/*  コメントアウト
friend double operator/(const int& n, const Number& num){
    return (num.kind == num.DOUBLE ? num.Double / n : num.Int / n);
}

friend double operator/const double& d, const Number& num){
    return (num.kind == num.DOUBLE ? num.Double / d : num.Int / d);
}
*/

// 比較演算子
operator <(const int& n, const Number& num){
    return num > n;
}
// 比較演算子
operator <=(const int& n, const Number& num){
    return num >= n;
}
// 比較演算子
operator >(const int& n, const Number& num){
    return num < n;
}
// 比較演算子
operator >=(const int& n, const Number& num){
    return num <= n;
}
// 比較演算子
operator !=(const int& n, const Number& num){
    return num != n;
}
// 比較演算子
operator ==(const int& n, const Number& num){
    return num == n;
}
#endif //NUMBER_H


4章：クラス

19. 練習問題

問題
    文字列クラスを作れ。文字列クラスの仕様は以下の通りとする。

  • メンバー変数として、NULLで終わるchar*型の変数を持つ
  • コンストラクタとして、const char*を受け取るコンストラクタを持つ。この場合、新しく文字数+1バイトの領域を確保し、文字列をコピーする。
  • コンストラクタとして、const charを受け取るコンストラクタを持つ。この場合、2バイトの領域を確保し、1バイト目に引数で渡された文字を、2バイト目にNULL文字を代入する。
  • コピーコンストラクタを持つ。この場合も、新しく文字数+1バイトの領域を確保し、文字列をコピーする。
  • デフォルトコンストラクタを持つ。この場合、1バイトの領域を確保し、NULLを代入する。
  • デストラクタを用意する。デストラクタは、確保した領域を開放する。
  • 代入演算子を用意する。引数は、const char*とする。
  • 代入演算子を用意する。引数は、const charとする。
  • 代入演算子を用意する。引数は、Stringとする。
  • const char*へのキャスト演算子を用意する。
  • +演算子、+=演算子を用意する。
  • 比較演算子を用意する。ただし、辞書順に比較する。
  • ostreamへの出力演算子(<<)を用意する。
  • istreamからの入力演算子(>>)を用意する。ただし、単語ごとに入力する。
  • istreamから1行入力する関数を用意する。
  • すべて大文字にする関数を用意する。
  • すべて小文字にする関数を用意する。
  • 空文字列(１文字もない文字列)かどうかを判断する関数を用意する。
  • 指定文字を返すoperator[]を用意する。
  • 文字数を返すGetLength関数を用意する。


4章：クラス

19. 練習問題

問題
    文字列クラスを作れ。文字列クラスの仕様は以下の通りとする。

  • メンバー変数として、NULLで終わるchar*型の変数を持つ
  • コンストラクタとして、const char*を受け取るコンストラクタを持つ。この場合、新しく文字数+1バイトの領域を確保し、文字列をコピーする。
  • コンストラクタとして、const charを受け取るコンストラクタを持つ。この場合、2バイトの領域を確保し、1バイト目に引数で渡された文字を、2バイト目にNULL文字を代入する。
  • コピーコンストラクタを持つ。この場合も、新しく文字数+1バイトの領域を確保し、文字列をコピーする。
  • デフォルトコンストラクタを持つ。この場合、1バイトの領域を確保し、NULLを代入する。
  • デストラクタを用意する。デストラクタは、確保した領域を開放する。
  • 代入演算子を用意する。引数は、const char*とする。
  • 代入演算子を用意する。引数は、const charとする。
  • 代入演算子を用意する。引数は、Stringとする。
  • const char*へのキャスト演算子を用意する。
  • +演算子、+=演算子を用意する。
  • 比較演算子を用意する。ただし、辞書順に比較する。
  • ostreamへの出力演算子(<<)を用意する。
  • istreamからの入力演算子(>>)を用意する。ただし、単語ごとに入力する。
  • istreamから1行入力する関数を用意する。
  • すべて大文字にする関数を用意する。
  • すべて小文字にする関数を用意する。
  • 空文字列(１文字もない文字列)かどうかを判断する関数を用意する。
  • 指定文字を返すoperator[]を用意する。
  • 文字数を返すGetLength関数を用意する。

解答
String.h
#ifndef _STRING_H_
#define _STRING_H_

#include<iostream.h>

class String{
private:
    char*    pStr;

    void copy(const char* src);
    void copy(const char& ch);
public:
    // コンストラクタ
    String();
    String(const char* src);
    String(const char& ch);
    String(const String& src);

    // デストラクタ
    ~String();

    // 代入演算子
    String& operator=(const char* src);
    String& operator=(const char& ch);
    String& operator=(const String& src);

    // キャスト演算子
    operator const char*()const;

    // 比較演算子
    bool operator< (const char* p)const;
    bool operator<=(const char* p)const;
    bool operator> (const char* p)const;
    bool operator>=(const char* p)const;
    bool operator==(const char* p)const;
    bool operator!=(const char* p)const;
    bool operator< (const String& s)const;
    bool operator<=(const String& s)const;
    bool operator> (const String& s)const;
    bool operator>=(const String& s)const;
    bool operator==(const String& s)const;
    bool operator!=(const String& s)const;

    // 比較演算子(外部関数)
    friend bool operator< (const char* p, const String& s);
    friend bool operator<=(const char* p, const String& s);
    friend bool operator> (const char* p, const String& s);
    friend bool operator>=(const char* p, const String& s);
    friend bool operator==(const char* p, const String& s);
    friend bool operator!=(const char* p, const String& s);

    // +=演算子
    String& operator+=(const char* p);
    String& operator+=(const String& s);

    // +演算子
    String operator+(const char* p)const;
    String operator+(const String& s)const;
    friend String operator+(const char* p, const String& s);

    // 入力演算子
    friend istream& operator>>(istream& is, String& s);
    istream& GetLine(istream& is);

    // 出力演算子
    friend ostream& operator<<(ostream& os, const String& s);

    // 大文字・小文字変換
    String& ToUpper();
    String& ToLower();

    // 指定文字を返す
    char& operator[](int n);

    // 空文字か？
    bool IsEmpty()const;

    // 文字数を返す
    int GetLength()const;
};

#endif //_STRING_H_

String.cpp

#include<string>    // strlen, strcmp, strcpy, strcatなど
#include"String.h"

// デフォルトコンストラクタ
String::String(){
    pStr = new char('\0');
}

// コピーコンストラクタ
String::String(const String& src){
    copy( src.pStr );
}

// コンストラクタ
String::String(const char& ch){
    copy( ch );
}

// コンストラクタ
String::String(const char* src){
    copy( src );
}

// デストラクタ
String::~String(){
    delete pStr;
}

// ヘルパー関数
void String::copy(const char* src){
    int len = strlen( src );
    pStr = new char[len + 1];
    strcpy(pStr, src);
}

// ヘルパー関数
void String::copy(const char& ch){
    pStr = new char[2];
    *(pStr+0) = ch;
    *(pStr+1) = '\0';
}

// 代入演算子
String& String::operator=(const char* src){
    delete pStr;
    copy( src );
    return *this;
}

// 代入演算子
String& String::operator=(const char& ch){
    delete pStr;
    copy( ch );
    return *this;
}
// 代入演算子
String& String::operator=(const String& src){
    delete pStr;
    copy( src.pStr );
    return *this;
}

// キャスト演算子
String::operator const char*()const{
    return pStr;
}

// 比較演算子
bool String::operator< (const char* p)const{
    return strcmp( pStr, p ) < 0;
}
// 比較演算子
bool String::operator<=(const char* p)const{
    return strcmp( pStr, p ) <= 0;
}
// 比較演算子
bool String::operator> (const char* p)const{
    return strcmp( pStr, p ) > 0;
}
// 比較演算子
bool String::operator>=(const char* p)const{
    return strcmp( pStr, p ) >= 0;
}
// 比較演算子
bool String::operator==(const char* p)const{
    return strcmp( pStr, p ) == 0;
}
// 比較演算子
bool String::operator!=(const char* p)const{
    return strcmp( pStr, p ) != 0;
}
// 比較演算子
bool String::operator< (const String& s)const{
    return strcmp( pStr, s.pStr ) < 0;
}
// 比較演算子
bool String::operator<=(const String& s)const{
    return strcmp( pStr, s.pStr ) <= 0;
}
// 比較演算子
bool String::operator> (const String& s)const{
    return strcmp( pStr, s.pStr ) > 0;
}
// 比較演算子
bool String::operator>=(const String& s)const{
    return strcmp( pStr, s.pStr ) >= 0;
}
// 比較演算子
bool String::operator==(const String& s)const{
    return strcmp( pStr, s.pStr ) == 0;
}
// 比較演算子
bool String::operator!=(const String& s)const{
    return strcmp( pStr, s.pStr ) != 0;
}

// 比較演算子(外部関数)
bool operator< (const char* p, const String& s){
    return strcmp( p, s.pStr ) < 0;
}
// 比較演算子(外部関数)
bool operator<=(const char* p, const String& s){
    return strcmp( p, s.pStr ) <= 0;
}
// 比較演算子(外部関数)
bool operator> (const char* p, const String& s){
    return strcmp( p, s.pStr ) > 0;
}
// 比較演算子(外部関数)
bool operator>=(const char* p, const String& s){
    return strcmp( p, s.pStr ) >= 0;
}
// 比較演算子(外部関数)
bool operator==(const char* p, const String& s){
    return strcmp( p, s.pStr ) == 0;
}
// 比較演算子(外部関数)
bool operator!=(const char* p, const String& s){
    return strcmp( p, s.pStr ) != 0;
}

// +=演算子
String& String::operator+=(const char* p){
    int len = strlen( pStr ) + strlen( p ) + 1;
    char* pTmp = pStr;
    pStr = new char[len];
    strcpy( pStr, pTmp );
    strcat( pStr, p );
    return *this;
}
// +=演算子
String& String::operator+=(const String& s){
    return *this+=s.pStr;
}

// +演算子
String String::operator+(const char* p)const{
    String str(*this);
    str+=p;
    return str;
}
// +演算子
String String::operator+(const String& s)const{
    String str(*this);
    str+=s.pStr;
    return str;
}
// +演算子(外部関数)
String operator+(const char* p, const String& s){
    String str(p);
    str+=s.pStr;
    return str;
}

// 入力演算子(外部関数)
istream& operator>>(istream& is, String& s){
    char buffer[512];    // 危険!!
    is >> buffer;
    s = buffer;
    return is;
}
// 入力演算子
istream& String::GetLine(istream& is){
    char buffer[512];    // 危険!!
    is.getline(buffer, sizeof(buffer));
    *this = buffer;
    return is;
}

// 大文字・小文字変換
String& String::ToUpper(){
    for( char *p = pStr ; *p ; p++ )
        *p = toupper( *p );
    return *this;
}
String& String::ToLower(){
    for( char *p = pStr ; *p ; p++ )
        *p = tolower( *p );
    return *this;
}

// 指定文字を返す
char& String::operator[](int n){
    return *(pStr+n);
}

// 空文字か？
bool String::IsEmpty()const{
    return *pStr == '\0';
}

// 文字数を返す
int String::GetLength()const{
    return strlen( pStr );
}

// 出力演算子
ostream& operator<<(ostream& os, const String& s){
    return os << s.pStr;
}


4章：クラス

20. クラス変数(静的変数)

    クラス変数とは、クラスのメンバー変数の一種であるが、どのインスタンスからも 同じ値を指すような変数です。
#include<iostream.h>

class TestClass{
public:
    static int     static_n;  // 宣言のみ
    int        non_static_n;
};

// staticな変数の実体は、クラス宣言の外で行う
int TestClass::static_n;

ostream& operator<<(ostream& os, const TestClass& tc){
    return os << "static_n : " << tc.static_n << "  non_static_n : " << tc.non_static_n << endl;
}
void main(){
    TestClass tc1;
    TestClass tc2;

    cout << "tc1 : " << tc1;
    cout << "tc2 : " << tc2;

    tc1.non_static_n = 100;
    tc1.    static_n = 50;

    cout << "tc1 : " << tc1;
    cout << "tc2 : " << tc2;
}

tc1 : static_n : 0  non_static_n : 4207454
tc2 : static_n : 0  non_static_n : 4227072
tc1 : static_n : 50  non_static_n : 100
tc2 : static_n : 50  non_static_n : 4227072

    インスタンスの１つであるtc1のクラス変数を変更すると、その変更内容が もう一つのインスタンスであるtc2にも及んでいます。これは複数のインスタンスから 同じ変数にアクセスしているからです。

    上記のようにクラス変数(静的変数)は、初期化しない場合は最初に0に初期化されています。 クラスのインスタンスであればデフォルトコンストラクタが使われます。もし、それ以外に 初期化を行いたければ、以下のように記述します。メンバーイニシャライザによる初期化は できません。

#include<iostream.h>

class TestClass{
public:
    static int     static_n;  // 宣言のみ
    int        non_static_n;
};

// staticな変数の実体は、クラス宣言の外で行う
// 明示的に初期化する
int TestClass::static_n = 10;

ostream& operator<<(ostream& os, const TestClass& tc){
    return os << "static_n : " << tc.static_n << "  non_static_n : " << tc.non_static_n << endl;
}
void main(){
    TestClass tc1;
    TestClass tc2;

    cout << "tc1 : " << tc1;
    cout << "tc2 : " << tc2;

    tc1.non_static_n = 100;
    tc1.    static_n = 50;

    cout << "tc1 : " << tc1;
    cout << "tc2 : " << tc2;
}

tc1 : static_n : 10  non_static_n : 4207454
tc2 : static_n : 10  non_static_n : 4227072
tc1 : static_n : 50  non_static_n : 100
tc2 : static_n : 50  non_static_n : 4227072


4章：クラス

    クラス変数の使い方は主に２種類あります。１つめはそのクラスの状態を 表す変数として、２つめはクラスの定数を表す場合です。

    下の例はクラスの状態を表す変数として静的変数を使用している例です。 マルチスレッドを利用していますが、マルチスレッドの詳しい説明は省きます。 クラス変数として「bPause」がtrueになれば、複数あるEnemyCharactorのスレッドが 一斉に一時停止になります。そして、「bGameOver」がtrueになれば、一斉に全ての スレッドが停止します。

    このようにEnemyCharactorクラスの全インスタンス共通の状態を表す使い方が １つめの使い方です。

    なお、EnemyCharactorには固有のID番号がつけられますが、次のID番号が何番に なるかを静的な変数「nNextID」として定義されています。これも共通の状態を表す という使い方になるでしょう・・・。たぶん。
#include<process.h>         // for_beginthread

class EnemyCharactor{
protected:
    static int  nNextID;
    static bool bPause;
    static bool bGameOver;

    int    nID;
public:
    // コンストラクタ
    EnemyCharactor(){
        nID = nNextID;
        nNextID++;
        StartThread();
    }

    // デストラクタ
    ~EnemyCharactor();

    // スレッドを開始する
    void StartThread(){
        _beginthread( Thread, 0, this );
    }

    // ゲームオーバー
    static void GameOver(){
        bGameOver = true;
    }

    // 一時停止
    static void Pause(){
        bPause = true;
    }

    // キャラクターを動かす
    void MoveCharactor();

    // スレッド本体
    static void Thread( void* arg ){
        EnemyCharactor* pThis = (EnemyCharactor*)arg;

        while( !bGameOver ){
            // 一時停止中だったら待つ
            while( bPause );

            // キャラクターを動かす
            pThis->MoveCharactor();
        }
    }
};

void main(){
    EnemyCharactor  chara1;
     :
     :
     :
}


4章：クラス

    クラスの定数を表す例を示します。

    以下の例では、エラーコードや、ファイルのオープンモードをクラス変数にして定義しています。 エラーコードを数字で返すよりもわかりやすくなっています。また、#defineしてもよいのですが、 クラス変数にすることで、Fileクラスのエラーコードであることが、明示的に示されるので、 #defineするよりもよい書き方といえます。(#defineすると名前が競合するおそれがあるため。)

    なお、静的変数の部分はアンダーラインで示しました。

File.h

#ifndef _FILE_H_
#define _FILE_H_

#include <stdio.h>
#include <stdlib.h>

class File{
public:
    // エラーコード
    const static int NO_ERROR;
    const static int ERR_FILE_NOT_FOUND;
    const static int ERR_FAIL_CREATE_FILE;
    const static int ERR_FAIL_WRITE_FILE;
    const static int ERR_FAIL_READ_FILE;
    const static int ERR_FAIL_CLOSE;
    const static int ERR_END_OF_FILE;
    const static int ERR_NO_OPEN;
    const static int ERR_INVALID_OPEN_MODE;

    // ファイルオープンモード
    const static char*const READ_ONLY;
    const static char*const WRITE_ONLY;
    const static char*const APPEND_ONLY;
    const static char*const READ_WRITE;
    const static char*const READ_APPEND;
    const static char*const INVALID_MODE;

private:
    FILE*        fp;        // ファイルポインタ
    const char*  mode;      // オープンモード

public:
    // コンストラクタ
    File() : fp(0) {}
    File(const char* FileName);

    // デストラクタ
    ~File();

    // ファイルオープン
    int Open(const char* FileName, const char* OpenMode = READ_ONLY);

    // ファイルクローズ
    int Close();

    // ファイル読み込み
    int Read(void* pBuffer, int nSize)const;

    // ファイル書き込み
    int Write(const void*const pBuffer, unsigned int nSize)const;

    // ファイルサイズ取得
    int GetFileSize()const;

    // オープンしているか？
    bool IsOpen()const;

    // オープンモードの取得
    const char* const GetOpenMode()const;
};
#endif //_FILE_H_

File.cpp

// エラーコード
const int File::NO_ERROR              =  0;
const int File::ERR_FILE_NOT_FOUND    = -1;
const int File::ERR_FAIL_CREATE_FILE  = -2;
const int File::ERR_FAIL_WRITE_FILE   = -3;
const int File::ERR_FAIL_READ_FILE    = -4;
const int File::ERR_FAIL_CLOSE        = -5;
const int File::ERR_END_OF_FILE       = -6;
const int File::ERR_NO_OPEN           = -7;
const int File::ERR_INVALID_OPEN_MODE = -8;

// ファイルオープンモード
const char* const File::READ_ONLY    = "r";
const char* const File::WRITE_ONLY   = "w";
const char* const File::APPEND_ONLY  = "a";
const char* const File::READ_WRITE   = "w+";
const char* const File::READ_APPEND  = "a+";
const char* const File::INVALID_MODE = 0;

// コンストラクタ
File::File(const char* FileName){
    Open(FileName);
}

// デストラクタ
File::~File(){
    // ファイルが開かれていれば閉じる
    if( fp )
        Close();
}

// ファイルオープン
int File::Open(const char* FileName, const char* OpenMode){
    // オープンモードのチェック
    if( OpenMode != READ_ONLY &&
        OpenMode != WRITE_ONLY &&
        OpenMode != APPEND_ONLY &&
        OpenMode != READ_WRITE &&
        OpenMode != READ_APPEND )
        return ERR_INVALID_OPEN_MODE;

    fp = fopen(FileName, OpenMode);

    // エラーじゃない？
    if( fp ){
        mode = OpenMode;
        return NO_ERROR;
    }

    // ファイルがない？
    if( OpenMode == READ_ONLY )
        return ERR_FILE_NOT_FOUND;

    // それ以外ならファイルが作れない
    return ERR_FAIL_CREATE_FILE;
}

// ファイルクローズ
int File::Close(){
    // オープンされていない？
    if( !fp )
        return ERR_NO_OPEN;

    // ファイルクローズ
    if( fclose(fp) == 0 ){
        fp = 0;
        return NO_ERROR;
    }

    // クローズ失敗
    return ERR_FAIL_CLOSE;
}

// ファイル読み込み
// 戻り値が0以上なら読めた
int File::Read(void* pBuffer, int nSize)const{
    // オープンされていない？
    if( !fp )
        return ERR_NO_OPEN;

    // モードのチェック
    if( mode == WRITE_ONLY || mode == APPEND_ONLY )
        return ERR_INVALID_OPEN_MODE;

    // 読み込み
    int nRet = fread(pBuffer, 1, nSize, fp );

    // 失敗？
    if( nRet == EOF )
        // ファイル終端？
        return ferror( fp ) ? ERR_END_OF_FILE : ERR_FAIL_READ_FILE;

    return nRet;
}

// ファイル書き込み
int File::Write(const void*const pBuffer, unsigned int nSize)const{
    // オープンされていない？
    if( !fp )
        return ERR_NO_OPEN;

    // モードのチェック
    if( mode == READ_ONLY )
        return ERR_INVALID_OPEN_MODE;

    // 書き込み
    return fwrite( pBuffer, 1, nSize, fp ) == nSize
        ? NO_ERROR
        : ERR_FAIL_WRITE_FILE;
}

// ファイルサイズ取得
int File::GetFileSize()const{
    // オープンされていない？
    if( !fp )
        return ERR_NO_OPEN;

    long current = ftell(fp);
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fseek(fp, current, SEEK_SET);
    return size;
}

// オープンしているか？
bool File::IsOpen()const{
    return fp ? true : false;
}

// オープンモードの取得
const char* const File::GetOpenMode()const{
    // オープンされていない？
    if( !fp )
        return INVALID_MODE;

    return mode;
}

main.cpp

#include<iostream.h>
#include"File.h"

void main(int argc, char** argv){
    File file;

    // 引数のチェック
    if( argc < 2 ){
        cerr << "usage : filetest filename" << endl;
        return;
    }

    // ファイルを開く
    if( file.Open(argv[1]) != File::NO_ERROR ){
        cerr << "File Not Found" << endl;
        return;
    }

    // サイズ取得
    int size = file.GetFileSize();

    // サイズ分のメモリーを確保
    char* buffer = new char[size+1];

    // 最後をNULLにする
    buffer[size] = '\0';

    // ファイルを読む
    file.Read(buffer, size);

    // ファイルクローズ
    file.Close();

    // 表示
    cout << buffer << endl;

    delete buffer;
}

C:\>filetest c:\config.sys
device=C:\WINDOWS\himem.sys
device=C:\WINDOWS\EMM386.EXE RAM
devicehigh=C:\WINDOWS\kkcfunc.sys
devicehigh=C:\WINDOWS\hrtimer.sys

dos=high,umb
lastdrive=q
C:\>


4章：クラス

21. 静的関数

    静的関数は、インスタンスを必要としない関数です。インスタンスを必要としないという ことは、「this」を使うことができません。「this」が使えないと言うことは、静的変数以外の メンバー変数を参照できないと言うことです。(インスタンスがないのだから当然ですが。)

    機能的に見て、そのクラスに入れるべきであるが、メンバー変数を参照する必要のない 関数は静的関数にします。

    また、そのクラスのインスタンス共通の状態などを示す静的変数のみを操作する場合も 静的関数にします。例えば、先ほどのEnemyCharactorクラス では、GameOver関数や、Pause関数は静的関数になっています。

    それでは、先ほどのFileクラスに、ファイル名を指定して ファイルサイズを取得する関数を追加してみます。ファイル名を指定するということは、 インスタンスを必要としません。したがって、静的関数になります。

File.h

#ifndef _FILE_H_
#define _FILE_H_

#include <stdio.h>
#include <stdlib.h>

class File{
public:
    // エラーコード
    const static int NO_ERROR;
    const static int ERR_FILE_NOT_FOUND;
    const static int ERR_FAIL_CREATE_FILE;
    const static int ERR_FAIL_WRITE_FILE;
    const static int ERR_FAIL_READ_FILE;
    const static int ERR_FAIL_CLOSE;
    const static int ERR_END_OF_FILE;
    const static int ERR_NO_OPEN;
    const static int ERR_INVALID_OPEN_MODE;

    // ファイルオープンモード
    const static char*const READ_ONLY;
    const static char*const WRITE_ONLY;
    const static char*const APPEND_ONLY;
    const static char*const READ_WRITE;
    const static char*const READ_APPEND;
    const static char*const INVALID_MODE;

private:
    FILE*        fp;        // ファイルポインタ
    const char*  mode;      // オープンモード

public:
    // コンストラクタ
    File() : fp(0) {}
    File(const char* FileName);

    // デストラクタ
    ~File();

    // ファイルオープン
    int Open(const char* FileName, const char* OpenMode = READ_ONLY);

    // ファイルクローズ
    int Close();

    // ファイル読み込み
    int Read(void* pBuffer, int nSize)const;

    // ファイル書き込み
    int Write(const void*const pBuffer, unsigned int nSize)const;

    // ファイルサイズ取得
    int GetFileSize()const;

    // オープンしているか？
    bool IsOpen()const;

    // オープンモードの取得
    const char* const GetOpenMode()const;

    // ファイルサイズの取得
    static int GetFileSize(const char*const FileName);
};
#endif //_FILE_H_

File.cpp

// エラーコード
const int File::NO_ERROR              =  0;
const int File::ERR_FILE_NOT_FOUND    = -1;
const int File::ERR_FAIL_CREATE_FILE  = -2;
const int File::ERR_FAIL_WRITE_FILE   = -3;
const int File::ERR_FAIL_READ_FILE    = -4;
const int File::ERR_FAIL_CLOSE        = -5;
const int File::ERR_END_OF_FILE       = -6;
const int File::ERR_NO_OPEN           = -7;
const int File::ERR_INVALID_OPEN_MODE = -8;

// ファイルオープンモード
const char* const File::READ_ONLY    = "r";
const char* const File::WRITE_ONLY   = "w";
const char* const File::APPEND_ONLY  = "a";
const char* const File::READ_WRITE   = "w+";
const char* const File::READ_APPEND  = "a+";
const char* const File::INVALID_MODE = 0;

// コンストラクタ
File::File(const char* FileName){
    Open(FileName);
}

// デストラクタ
File::~File(){
    // ファイルが開かれていれば閉じる
    if( fp )
        Close();
}

// ファイルオープン
int File::Open(const char* FileName, const char* OpenMode){
    // オープンモードのチェック
    if( OpenMode != READ_ONLY &&
        OpenMode != WRITE_ONLY &&
        OpenMode != APPEND_ONLY &&
        OpenMode != READ_WRITE &&
        OpenMode != READ_APPEND )
        return ERR_INVALID_OPEN_MODE;

    fp = fopen(FileName, OpenMode);

    // エラーじゃない？
    if( fp ){
        mode = OpenMode;
        return NO_ERROR;
    }

    // ファイルがない？
    if( OpenMode == READ_ONLY )
        return ERR_FILE_NOT_FOUND;

    // それ以外ならファイルが作れない
    return ERR_FAIL_CREATE_FILE;
}

// ファイルクローズ
int File::Close(){
    // オープンされていない？
    if( !fp )
        return ERR_NO_OPEN;

    // ファイルクローズ
    if( fclose(fp) == 0 ){
        fp = 0;
        return NO_ERROR;
    }

    // クローズ失敗
    return ERR_FAIL_CLOSE;
}

// ファイル読み込み
// 戻り値が0以上なら読めた
int File::Read(void* pBuffer, int nSize)const{
    // オープンされていない？
    if( !fp )
        return ERR_NO_OPEN;

    // モードのチェック
    if( mode == WRITE_ONLY || mode == APPEND_ONLY )
        return ERR_INVALID_OPEN_MODE;

    // 読み込み
    int nRet = fread(pBuffer, 1, nSize, fp );

    // 失敗？
    if( nRet == EOF )
        // ファイル終端？
        return ferror( fp ) ? ERR_END_OF_FILE : ERR_FAIL_READ_FILE;

    return nRet;
}

// ファイル書き込み
int File::Write(const void*const pBuffer, unsigned int nSize)const{
    // オープンされていない？
    if( !fp )
        return ERR_NO_OPEN;

    // モードのチェック
    if( mode == READ_ONLY )
        return ERR_INVALID_OPEN_MODE;

    // 書き込み
    return fwrite( pBuffer, 1, nSize, fp ) == nSize
        ? NO_ERROR
        : ERR_FAIL_WRITE_FILE;
}

// ファイルサイズ取得
int File::GetFileSize()const{
    // オープンされていない？
    if( !fp )
        return ERR_NO_OPEN;

    long current = ftell(fp);
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fseek(fp, current, SEEK_SET);
    return size;
}

// オープンしているか？
bool File::IsOpen()const{
    return fp ? true : false;
}

// オープンモードの取得
const char* const File::GetOpenMode()const{
    // オープンされていない？
    if( !fp )
        return INVALID_MODE;

    return mode;
}

// ファイルサイズの取得
int File::GetFileSize(const char*const FileName){
    // ファイルオープン
    File* f = fopen(FileName, ReadOnly);

    // 開けなければエラー
    if( !f )
        return ERR_FILE_NOT_FOUND;

    long current = ftell(f);
    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, current, SEEK_SET);

    // ファイルクローズ
    fclose(f);

    return size;
}


4章：クラス

22. クラスと関数ポインタ

    クラスのメンバー関数を関数ポインタに代入する場合、そのメンバー関数は静的関数 でなければなりません。したがって下記の例はエラーになります。

ダメな例

class Test{
public:
    // メンバー変数
    int number;

    // 関数ポインタ
    int (*m_pf)();

    // メンバー関数
    int getNumber(){
        return number;
    }

    // コンストラクタ
    Test(){
       m_pf = getNumber;    // エラー
    }
};

    この場合、引数に「this」を渡して、このポインタを経由してメンバー変数にアクセスするようにします。

良いな例

class Test{
public:
    // メンバー変数
    int number;

    // 関数ポインタ
    int (*m_pf)(Test*);

    // メンバー関数
    static int getNumber(Test* pThis){
        return pThis->number;
    }

    // コンストラクタ
    Test(){
       m_pf = getNumber;    // OK
    }
};


5章：クラスの包含

1. 包含とは

    前章のStringクラスのメンバー変数は文字列を指す char*でした。包含とはあるクラスのメンバー変数として、別のクラスのインスタンスを 持つことを言います。

    メンバー変数としてクラスを持つ持ち方は３つあります。1)インスタンスを持つ、 2)ポインタで持つ、3)参照で持つ場合です。

インスタンスを持つ ポインタを持つ 参照を持つ
class A_Class{
    B_Class B;
};

class A_Class{
    B_Class* pB;
};

class A_Class{
    B_Class& rB;
};
A_Classのインスタンスが存在する間はずっとB_Classのインスタンスも存在する。 逆にB_Classのインスタンスがなくなってしまえば、A_Classは機能しなくなるような場合に 用いられる。 A_Classのインスタンスが存在する間も、B_Classのインスタンスが存在する場合と 存在しない場合があるような場合に用いられる。 B_Classのインスタンスを複数のA_Classのインスタンスが使い回す場合に用いられる。 この場合、B_Classのインスタンスの寿命がA_Classのインスタンスの寿命よりも 永いことが保証されていないと危険である。

例）自動車クラスはエンジンクラスを包含するなど。
(この場合、エンジンがなくなれば自動車ではなくなる) 		例）自動車クラスは運転手クラスを包含するなど。
(この場合、運転手がいてもいなくても、自動車は自動車であることにかわりはない)

    上の用途の例はあくまでも例であり、用途やプログラマー(設計者)が一番適切な 包含の仕方を決める。


5章：クラスの包含

2. クラスの作成・破壊

    以下のようなプログラムがあったとします。
#include<iostream.h>

class A_Class{
    int  n;
    char ch;

public:
    // コンストラクタ
    A_Class(){
        cout << "A_Class's constructor" << endl;
        n  = 0;
        ch = 'A';
    }

    // デストラクタ
    ~A_Class(){
        cout << "A_Class's destructor" << endl;
    }
};

void main(){
    cout << "main start" << endl;
    A_Class a;
    cout << "main end" << endl;
}

main start
A_Class's constructor
main end
A_Class's destructor

    この場合、main関数内でA_Classのインスタンスを作成していますが、 インスタンスを作成するには、以下のような順番で作成されます。

  • A_Classのメモリー領域(メンバー変数はint型とchar型なので5バイト分だが、4の倍数になるので8バイト分)が確保される。
  • 変数が作られる。(変数の中身は不定)
  • コンストラクタ(A_Class())が呼ばれる。

    次にmain関数が終わると、A_Classのインスタンスのスコープからはずれるので、 A_Classのインスタンスが破壊されます。これは以下のような手順で行われます。
  • デストラクタ(~A_Class())が呼ばれる。
  • 変数が破壊される。
  • メモリー領域が開放される。


5章：クラスの包含

    では次に包含クラスがある場合です。以下のようなプログラムがあったとします。
#include<iostream.h>

class A_Class{
    int  n;
    char ch;

public:
    // コンストラクタ
    A_Class(){
        cout << "A_Class's constructor" << endl;
        n  = 0;
        ch = 'A';
    }

    // デストラクタ
    ~A_Class(){
        cout << "A_Class's destructor" << endl;
    }
};

class B_Class{
    int     n;
    char    ch;
    A_Class a;

public:
    // コンストラクタ
    B_Class(){
        cout << "B_Class's constructor" << endl;
        n  = 0;
        ch = 'A';
    }

    // デストラクタ
    ~B_Class(){
        cout << "B_Class's destructor" << endl;
    }
};

void main(){
    cout << "main start" << endl;
    B_Class B;
    cout << "main end" << endl;
}

main start
A_Class's constructor
B_Class's constructor
main end
B_Class's destructor
A_Class's destructor

    この場合、main関数内でB_Classのインスタンスを作成していますが、 インスタンスを作成するには、以下のような順番で作成されます。

  • B_Classのメモリー領域(A_Classが8バイト+int型+char型で、4の倍数になるので16バイト)が確保される。
  • B_Classのメンバー変数(含 A_Classのインスタンス)が作られる。
  • A_Classのインスタンスが作成されるということは、A_Classのメンバー変数が作られる。
  • A_Classのメンバー変数が作られたので、A_Classのコンストラクタが呼ばれる。
  • B_Classのメンバー変数が作り終わったことになり、B_Classのコンストラクタが呼ばれる。

    次にmain関数が終わると、B_Classのインスタンスのスコープからはずれるので、 A_Classのインスタンスが破壊されます。これは以下のような手順で行われます。
  • B_Classのデストラクタが呼ばれる。
  • B_Classのメンバー変数(含 A_Classのインスタンス)が破壊される。
  • A_Classのインスタンスが破壊されると言うことは、A_Classのデストラクタが呼ばれる。
  • A_Classのメンバー変数が破壊される。
  • B_Classのメモリー領域が開放される。

    このような順番だからこそ、コンストラクタやデストラクタでメンバー変数をさわることができる。 もし、メンバー変数が作られる前にコンストラクタが呼ばれれば、コンストラクタで メンバー変数をさわることができなくなってしまう。


5章：クラスの包含

3. メンバーイニシャライザ

    前ページの例のように包含する場合、 B_Classのメンバー変数、A_Class aはデフォルトコンストラクタしか呼ぶことができません。 もしA_Classにデフォルトコンストラクタがない場合や、他のコンストラクタを呼びたい場合は メンバーイニシャライザを使います。メンバーイニシャライザの部分をアンダーラインで 示した。
#include<iostream.h>

class A_Class{
    int  n;
    char ch;

public:
    // デフォルトコンストラクタ
    A_Class(){
        cout << "A_Class's default constructor" << endl;
        n  = 0;
        ch = 'A';
    }

    // コンストラクタ
    A_Class(int i){
        cout << "A_Class's constructor " << i << endl;
        n  = i;
        ch = 'A';
    }

    // デストラクタ
    ~A_Class(){
        cout << "A_Class's destructor" << endl;
    }
};

class B_Class{
    int     n;
    char    ch;
    A_Class a;

public:
    // デフォルトコンストラクタ
    B_Class(){
        cout << "B_Class's constructor" << endl;
        n  = 0;
        ch = 'A';
    }
    // コンストラクタ
    B_Class(int i) : a(i){
                   // ↑メンバーイニシャライザ
                   //   メンバー変数aをiで初期化する
        cout << "B_Class's constructor  " << i << endl;
        n  = i;
        ch = 'A';
    }

    // デストラクタ
    ~B_Class(){
        cout << "B_Class's destructor" << endl;
    }
};

void main(){
    cout << "main start" << endl;
    B_Class b1;
    cout << endl;
    B_Class b2(10);
    cout << "main end" << endl;
}

main start
A_Class's default constructor
B_Class's constructor

A_Class's constructor 10
B_Class's constructor  10
main end
B_Class's destructor
A_Class's destructor
B_Class's destructor
A_Class's destructor

    メンバーイニシャライザがある場合は以下のような手順でクラスが作成されます。

  • B_Classのメモリー領域(A_Classが8バイト+int型+char型で、4の倍数になるので16バイト)が確保される。
  • メンバー変数(含 A_Classのインスタンス)が作られる。
    (このとき、B_Classの対応するコンストラクタにメンバーイニシャライザがあれば、 そのメンバーイニシャライザによって初期化される。)
  • A_Classのメンバー変数が作られる。
  • A_Classのメンバー変数が作られたので、A_Classのコンストラクタが呼ばれる。
    (このとき、先ほど調べたメンバーイニシャライザがあれば、対応するA_Classのコンストラクタを呼び、なければA_Classのデフォルトコンストラクタを呼ぶ。)
  • B_Classのメンバー変数が作り終わったことになり、B_Classのコンストラクタが呼ばれる。


5章：クラスの包含

    メンバーイニシャライザは、包含クラスの初期化(どのコンストラクタを呼ぶか)を 指定するだけでなく、普通(int型、ポインタ型など)のメンバー変数の初期化にも使用できます。 したがって、前ページの例は、以下のようにすることもできます。 なお、メンバーイニシャライザを複数記述する際はカンマ(,)で区切ります。

#include<iostream.h>

class A_Class{
    int  n;
    char ch;

public:
    // デフォルトコンストラクタ
    A_Class() :
    n(0),
    ch('A')
    {
        cout << "A_Class's default constructor" << endl;
    }

    // コンストラクタ
    A_Class(int i):
    n(i),
    ch('A')
    {
        cout << "A_Class's constructor " << n << endl;
    }

    // デストラクタ
    ~A_Class(){
        cout << "A_Class's destructor" << endl;
    }
};

class B_Class{
    int     n;
    char    ch;
    A_Class a;

public:
    // デフォルトコンストラクタ
    B_Class():
    n(0),
    ch('A')
    {
        cout << "B_Class's constructor" << endl;
    }
    // コンストラクタ
    B_Class(int i):
    a(i),
    n(i),
    ch('A')
    {
        cout << "B_Class's constructor  " << n << endl;
    }

    // デストラクタ
    ~B_Class(){
        cout << "B_Class's destructor" << endl;
    }
};

void main(){
    cout << "main start" << endl;
    B_Class b1;
    cout << endl;
    B_Class b2(10);
    cout << "main end" << endl;
}

main start
A_Class's default constructor
B_Class's constructor

A_Class's constructor 10
B_Class's constructor  10
main end
B_Class's destructor
A_Class's destructor
B_Class's destructor
A_Class's destructor

    メンバーイニシャライザが複数ある場合の実行順番はコンパイラによります。 一般に矛盾のないような順番に並び替えてくれます。

    下記の３つの例のうち２つめは、まだ初期化していないnでaを 初期化しているような気がしますが、多くのコンパイラでは、これを並び替えて くれます。したがって、以下の３つのコンストラクタはすべて同じように動作します。 ただし、１つめのように書くことを推奨します。
class B_Class{
    // コンストラクタ
    B_Class(int i) :
    a(i),
    n(i),
    ch('A')
    {
        cout << "B_Class's constructor  " << n << endl;
    }

    // コンストラクタ
    B_Class(int i) :
    a(n),
    n(i),
    ch('A')
    {
        cout << "B_Class's constructor  " << n << endl;
    }

    // コンストラクタ
    B_Class(int i) :
    n(i),
    a(n),
    ch('A')
    {
        cout << "B_Class's constructor  " << n << endl;
    }
};


5章：クラスの包含

    メンバー変数の初期化は、コンストラクタで行うよりも、メンバーイニシャライザを 使用して初期化するべきです。
#include<iostream.h>
#include"String.h"

class Class_A{
    String str;

public:
    // コンストラクタ(よい例)
    Class_A(const char* p) :
    str(p)
    {
        cout << "Class_A's constructor " << str << endl;
    }

    // コンストラクタ(悪い例)
    Class_A(const char* p)
    {
        str = p;
        cout << "Class_A's constructor " << str << endl;
    }
};

void main(){
    Class_A a("ABCDEF");
}

    メンバーイニシャライザ(上記の「よい例」)を用いると、Stringクラスのコンストラクタは 「const char*」を伴うコンストラクタが呼ばれ、strが初期化されます。しかし メンバーイニシャライザを使わない場合(上記の「悪い例」)では、Stringクラスの デフォルトコンストラクタが呼ばれた後で、Stringクラスの代入演算子が呼ばれます。 したがって、一見動作は同じに見えますが、効率が悪いのです。


5章：クラスの包含

4. ポインタによる包含

    前ページでは、インスタンスを包含する例を示しました。 ここではポインタによる包含を紹介します。

    ポインタによる包含は一番融通の利く包含方法です。包含するクラスと包含されるクラスの インスタンスの寿命が同じ場合(インスタンスを包含する場合と同じ)は、コンストラクタで newして、デストラクタでdeleteすればよい。また、自動車とドライバーの関係のように、 包含するクラスのインスタンスよりも包含されるクラスのインスタンスの方が寿命が 短い場合も、必要に応じてnewして、必要に応じてdeleteすればよい。ただしこの場合、 包含するクラスのデストラクタで、包含するクラスのインスタンスをdeleteすることを 忘れないようにする必要がある。最後に、包含するクラスのインスタンスの方が、 包含されるクラスのインスタンスの寿命よりも短い場合も、ポインタを使えば可能になる。

    ここではポインタでしか実現できない、自動車とドライバーの関係について説明します。
#include<iostream.h>
#ifdef  NULL
#undef  NULL
#endif//NULL
#define NULL 0

class A_Class{
    int  n;
    char ch;

public:
    // コンストラクタ
    A_Class(int i) :
        n(i),
        ch('A')
    {
        cout << "A_Class's constructor " << i << endl;
    }

    // デストラクタ
    ~A_Class(){
        cout << "A_Class's destructor" << endl;
    }
};

class B_Class{
    int      n;
    char     ch;
    A_Class* pa;

public:
    // デフォルトコンストラクタ
    B_Class() :
        pa( NULL ),
        n(0),
        ch('A')
    {
        cout << "B_Class's constructor" << endl;
    }
    // コンストラクタ
    B_Class(int i) :
        pa( new A_Class(i) ),
        n(i),
        ch('A')
    {
        cout << "B_Class's constructor  " << i << endl;
    }

    // デストラクタ
    ~B_Class(){
        cout << "B_Class's destructor" << endl;
        if( pa )
            delete pa;
    }

    // A_Classのインスタンスを作る
    void MakeInstance(int i){
        if( pa )
            delete pa;
        pa = new A_Class(i);
    }

    // A_Classのインスタンスを消す
    void DeleteInstance(){
        if( pa )
            delete pa;
        pa = NULL;
    }
};

void main(){
    cout << "main start" << endl;
    B_Class b1;
    b1.MakeInstance(100);
    b1.DeleteInstance();
    b1.MakeInstance(-1);
    cout << "main end" << endl;
}

main start
B_Class's constructor
A_Class's constructor 100
A_Class's destructor
A_Class's constructor -1
main end
B_Class's destructor
A_Class's destructor

    このように、ポインタを利用してクラスを包含する場合、インスタンスがない場合は 常にポインタをNULL(0)にしておき、インスタンスがある場合はそのポインタを保持する。 デストラクタでは、そのポインタがNULLでなければ、deleteする。

    今後、マルチスレッドにする予定がある場合は、このやり方は危険です。 このページでは今のところマルチスレッドを解説する予定はないので、説明は省きます。


5章：クラスの包含

5. 参照による包含

    参照によるクラスの包含は、包含されるクラスのインスタンスの寿命が、包含する クラスのインスタンスの寿命よりも永い場合に使われる。特に、包含されるクラスの インスタンスを包含するクラスの複数のインスタンスが使う場合に有効である。

#include<iostream.h>
#ifdef  NULL
#undef  NULL
#endif//NULL
#define NULL 0

class A_Class{
    int  n;
    char ch;

public:
    // コンストラクタ
    A_Class(int i) :
        n(i),
        ch('A')
    {
        cout << "A_Class's constructor " << i << endl;
    }

    // デストラクタ
    ~A_Class(){
        cout << "A_Class's destructor" << endl;
    }
};

class B_Class{
    int      n;
    char     ch;
    A_Class& ra;

public:
    // コンストラクタ
    B_Class(A_Class& a) :
        ra( a ),
        n(0),
        ch('A')
    {
        cout << "B_Class's constructor  " << endl;
    }

    // デストラクタ
    ~B_Class(){
        cout << "B_Class's destructor" << endl;
    }
};

void main(){
    cout << "main start" << endl;
    A_Class a(100);
    B_Class b1(a);
    B_Class b2(a);
    cout << "main end" << endl;
}

main start
A_Class's constructor 100
B_Class's constructor
B_Class's constructor
main end
B_Class's destructor
B_Class's destructor
A_Class's destructor

    このように、参照を利用してクラスを包含する場合、参照されるインスタンス (この場合、A_Class)は、B_Classのインスタンスの寿命よりも永い必要がある。 例えば、下記のようにした場合、B_Classのインスタンスb1, b2で、まだA_Classの インスタンスpaを参照しているにもかかわらず、A_Classのインスタンスを削除してしまって いる。この場合、もしB_Classの中で、A_Classの参照にアクセスしようとすると 実行時にエラーがでたり、何が起こるかわからない状況になる。
void main(){
    cout << "main start" << endl;
    A_Class* pa = new A_Class(100);
    B_Class b1(*pa);
    B_Class b2(*pa);
    delete pa;
    cout << "main end" << endl;
}

    メンバー変数に参照を持つクラスでは、メンバーイニシャライザが必ず必要になる。 なぜならば、参照ははじめに必ず初期化を行わなくてはならないからです。


5章：クラスの包含

6. 練習問題

問題
    では、今までの復習としてVectorクラスを作ってみることにします。Vectorクラスは 配列のことをいいます。Stackクラスのようにコンストラクタの 引数として配列の大きさをもらい、コンストラクタの中で配列をnewします。operator[]を 作成し、これを用いて、代入や取得をできるようにします。そしてデストラクタでは newしたものをdeleteします。ただし、配列はテンプレートクラスにします。

    復習ですので、4章の前半の部分の知識で作成できるはずです。 特に代入演算子、コピーコンストラクタ を忘れずに実装してください。

    次に、Vectorクラスを包含して、RingBufferクラスを作ります。例えばバッファー数 が10のリングバッファーでは、データを追加していきますが、最新の10個分のデータだけを格納し、 それよりも古いデータは、順次削除します。このようなバッファー構造をリングバッファーといいます。

    RingBufferクラスのコンストラクタはバッファーの数を引数に取ります。そしてその数で Vectorクラスを初期化します。add関数はデータを追加します。get関数は、引数にint型をとり、 リングバッファー内の古いものから数えて、n番目のデータを返します。

    さらにこのRingBufferクラスを用いて、UNIXのTAILコマンドのようなプログラムを作ります。 このプログラムは、ファイル名を引数にとって実行すると、ファイルの最後の10行だけを 表示するプログラムです。


5章：クラスの包含

6. 練習問題

問題
    では、今までの復習としてVectorクラスを作ってみることにします。Vectorクラスは 配列のことをいいます。Stackクラスのようにコンストラクタの 引数として配列の大きさをもらい、コンストラクタの中で配列をnewします。operator[]を 作成し、これを用いて、代入や取得をできるようにします。そしてデストラクタでは newしたものをdeleteします。ただし、配列はテンプレートクラスにします。

    復習ですので、4章の前半の部分の知識で作成できるはずです。 特に代入演算子、コピーコンストラクタ を忘れずに実装してください。

    次に、Vectorクラスを包含して、RingBufferクラスを作ります。例えばバッファー数 が10のリングバッファーでは、データを追加していきますが、最新の10個分のデータだけを格納し、 それよりも古いデータは、順次削除します。このようなバッファー構造をリングバッファーといいます。

    RingBufferクラスのコンストラクタはバッファーの数を引数に取ります。そしてその数で Vectorクラスを初期化します。add関数はデータを追加します。get関数は、引数にint型をとり、 リングバッファー内の古いものから数えて、n番目のデータを返します。

    さらにこのRingBufferクラスを用いて、UNIXのTAILコマンドのようなプログラムを作ります。 このプログラムは、ファイル名を引数にとって実行すると、ファイルの最後の10行だけを 表示するプログラムです。

解答
Vector.h

/////////////////////////////////////////////////////////////////////////////
//
//    テンプレートベクタークラス(動的メモリ割当版)定義
//

// ２重取り込みを防止する
#ifndef  _VECTOR_H_
#define  _VECTOR_H_

#include<process.h>
#include<iostream.h>

template<class Type>
class Vector{
protected:
    // 代入演算子とコピーコンストラクタから呼ばれる
    // コピーもとのメンバー変数size、spをコピーし、
    // 新しい配列を割り当て、配列の中身をコピーする
    void copy( const Vector<Type>& src ){
        // サイズをコピーする
        size  = src.size;

        // サイズ分の配列を作る
        vect = new Type[size];

        // 配列の中身をコピーする
        for( int i = 0 ; i < size ; i++ )
            vect[i] = src.vect[i];
    }

    ////////////////////////////////////////////////////
    // 管理情報

    // 配列サイズ
    int size;

    // 配列本体
    Type *vect;

public:
    // コンストラクタ：
    // Vectorオブジェクトが定義された時に自動的に呼び出される
    Vector( int sz = 100 ){
        // コンストラクタ引数を省略すると、スタックサイズは100となる
        // スタックサイズを保存する
        size = sz ;

        // スタックの実体(size個のType配列)を自由記憶上に割り当てる
        vect = new Type[size] ;
    }

    // コピーコンストラクタ
    Vector( const Vector<Type>& src ){
        copy( src ) ;
    }

    // デストラクタ：
    // Stack オブジェクトが削除される時に自動的に呼び出される
    virtual ~Vector(){
        //スタック本体を削除する
        delete [ ] vect ;
    }

    //
    // operator[]
    Type& operator[](int index){
        // サイズのチェック
        if( index < 0 || index >= size ){
            cerr << "配列参照のインデックス値が不正です" << endl;
            exit(1);
        }
        return *(vect + index);
    }

    // 現在のスタック長を獲得する
    int get_length()const{
        return size;
    }

    // 代入演算子
    Vector<Type>& operator=( const Vector<Type>& src ){
        // 自己代入でなければ
        if( &src != this ){
            // 現在自分が保有しているスタックを削除する
            delete [] vect;

            // コピー元の配列をコピーする
            copy( src );
        }
        return *this;
    }
};

#endif //_VECTOR_H_

RingBuffer.h

#ifndef RINGBUFFER_H
#define RINGBUFFER_H

#include"Vector.h"

template<class Type>
class RingBuffer{
private:
    // ベクター本体
    Vector<Type>   Buffer;      // バッファー本体
    int            Size;        // バッファーサイズ
    int            IndexAdd;    // 次に格納する場所を表すインデックス
    int            IndexGet;    // 一番古いデータが格納されているインデックス
    int            Length;      // 現在格納されている要素数
public:
    // コンストラクタ
    RingBuffer(int sz = 10) :
        Buffer(sz),
        Size(sz),
        IndexAdd( 0 ),
        IndexGet( 0 ),
        Length( 0 )
    {}

    // データを格納する
    void add(const Type& data){
        // データを格納
        Buffer[IndexAdd++] = data;

        // 次に格納するインデックスを更新する
        IndexAdd %= Size;

        // 格納されている要素数を増やす
        if( Length < Size )
            Length++;

        // もっとも古いデータを指すインデックスを更新する
        IndexGet = Length == Size ? IndexAdd : 0;
    }

    // データを取得する
    Type& get(int Index){
        // 引数のチェック
        if( Length <= Index ){
            cerr << "Out of Range" << endl;
            exit(1);
        }

        return Buffer[ (IndexGet+Index) % Size ];
    }

    // 要素数を返す
    int GetLength()const{
        return Length;
    }
};

#endif //RINGBUFFER_H

#include"RingBuffer.h"
#include"String.h"    // String.hは4-19で作ったもの

#include <stdlib.h>
#include <iostream.h>
#include <fstream.h>

// tail 処理関数のプロトタイプ
void tail( istream& is, int size ) ;

/////////////////////////////////////////////////////////////////////////////
//    使い方表示
void usage( ){
    cerr << "usage : mytail [ -number ] [ file... ]\n" ;
}

///////////////////////////////////////////////////////////////////////////////
//    メイン
int main( int argc, char** argv ){
    // 出力行数：デフォルトは10行
    int output_lineno = 10 ;

    // オプションの取得
    if( argc >= 2 && argv[1][0] == '-' ){
        // 出力行数を獲得する
        output_lineno = atoi( &argv[1][1] ) ;
        if( output_lineno <= 0 ){
            usage( ) ;
            return 1 ;
        }

        // 引数を調整する
        argc-- ; argv++ ;
    }

    // 標準入力から読み込む時
    if( argc == 1 ){
        // 処理本体は tail 関数に任せる
        tail( cin, output_lineno ) ;
    }

    // ファイルから読み込む時
    else{
        for( int i = 1 ; i < argc ; i++ ){
            // 入力ファイルストリームを準備する
            // オブジェクト名はifs
            // ファイル名は順次argvに格納されている
            ifstream ifs( argv[i] ) ;

            tail( ifs, output_lineno ) ;
        }
    }

    // 正常終了
    return 0 ;
}

////////////////////////////////////////////////////////////////////////////
//     tail 処理関数

void tail( istream& is, int size ){
    // 出力行数分のリングバッファ
    // String型のリングバッファを獲得する
    // オブジェクト名はring_buf, サイズは size
    RingBuffer<String> ring_buf( size ) ;

    // 入力行
    String line ;

    // １行ずつリングバッファに読み込む
    while( line.GetLine( is ) ){
        // リングバッファに詰め込む
        ring_buf.add( line ) ;
    }

    // リングバッファに残っている行を順次出力する
    int length = ring_buf.GetLength();
    for( int i = 0 ; i < length ; i++ )
        cout << ring_buf.get(i) << endl;
}


6章：継承

1. 継承とは

    継承とは、あるクラス(A_Class)があるクラス(B_Class)の一種である場合、A_Classは B_Classを継承していると言います。そして、A_ClassはB_CLassの 子クラス、サブクラス、派生クラスと言い、逆にB_ClassはA_Classの 親クラス、スーパークラス、基底クラスと言います。

    これをC++で定義すると以下のようになります。
class B_Class{
    :
    :
};

class A_Class : public class B_Class
    :
    :
};

    「一種である」というのは、下記の図形の例のように「楕円」や「線」や「多角形」は 「図形」の一種であり、「円」は「楕円」の一種。「直線」は「線」の一種であるし、 「三角形」や「四角形」は「多角形」の一種であるということである。

    つまり、「三角形」や「四角形」は「多角形」を継承していると言い、「三角形」や「四角形」は 「多角形」の子クラス(サブクラス)であると言い、「多角形」は「三角形」や「四角形」の 親クラス(スーパークラス、基底クラス)であるという。

    また、「正方形」は「菱形」と「長方形」の両方の特別な場合であり、このような 継承の仕方を多重継承という。Javaのようにオブジェクト指向言語でも多重継承をサポート していない言語も多いが、C++では多重継承をサポートしている。しかし、あまり使われない。

    継承すると、親クラスのメンバー変数、メンバー関数は、その子クラスでも使用できるようになる。 子クラスでは親クラスのメンバー関数をオーバーライドすることができるため、 子クラスで実相をより高機能化することができる。


6章：継承

    いよいよ継承についてです。前章の練習問題で作った Vectorクラスにソート機能を追加してみることにします。ソートなしのVectorクラスが あっても別に良いので、Vectorクラスを改良してソート機能を追加するのではなく、 Vectorクラスを継承して作ってみます。つまりソート機能付ベクタークラスは、 Vectorクラスの一種であると考えるわけです。

    継承するには以下のように記述します。

enum COLOR{
    black,
    white,
    red,
    green,
    yellow,
    blue
};

class Pencil{
    int    length;

    void   draw();	// 黒で絵を描く
};

class ColorPen : public Pencil{
    Color  color;

    void   draw();	// colorで絵を描く
};
    実際にソート機能付Vectorクラスを作るのは次のページ になります。

    余談ですが、私が継承を教えるときには、子クラスのことを「進化したクラス」と 教えています。「進化した」というのは一般的な言葉ではありませんが、色々な意味で 「進化した」という言葉があてはまるからです。子クラスのことを「進化した」と言っている 人がいたら、きっとその人は私に継承について教わった人か、その人から教わったか、 またはこのページを見た人でしょう。数年後に一般的に使われるようになっていたら 面白いですね。

    なぜ進化したクラスなのかと言いますと、

  • 親クラスよりも機能が上である
  • 親クラスから生まれたクラスである
だからです。


6章：継承

2. スーパークラスのコンストラクタ

    クラスを継承して子クラスを作る場合、そのほとんどが親クラスのコンストラクタを 明示的に呼び出す必要があります。つまり親クラスのデフォルトコンストラクタ以外を 呼び出したい場合などです。

    Vectorクラスと、ソート機能付Vectorクラス(SortableVectorクラス)の場合を考えてみます。 SortableVectorクラスのコンストラクタに、配列の大きさを指定すると、その引数を親クラスの コンストラクタに通知しなければなりません。その場合はメンバーイニシャライザ を使います。

    では、SortableVectorクラスを作ってみることにします。

#ifndef  SORTABLEVECTOR_H
#define  SORTABLEVECTOR_H

// 先程作ったVectorクラス
#include "Vector.h"

template<class Type>
class SortableVector : public Vector<Type>{
public:
    // コンストラクタ
    SortableVector(int sz) : Vector<Type>(sz){}

    // ソートする
    void sort(){
        quick_sort(0, size-1);
    }

private:
    // クイックソート
    void quick_sort( int min, int max ){
        // 幅１以下の時、することは何もない
        if( min >= max  )
            return;

        // ピボットを選ぶ
        Type pibot = vect[min] ;

        // 区間 [min+1, max] を以下の３つの区間に分割する：
        // 1. 区間 [min+1, left-1] 内の全要素はピボット以下である
        // 2. 区間 [right+1, max] 内の全要素はピボット以上である
        // 3.  区間 [left, right] は現在調査中である

        int left = min+1 ;
        int right = max ;

        // ピボットが挿入される位置
        int pibot_pos ;

        while( 1 ){
            // ピボットよりも大きな要素を左から探す

            for( int i = left ; i <= right ; i++ ){
                if( vect[i] > pibot ) {
                    break ;
                }
            }

            // 大きな要素が見つからない：
            // つまり、min+1 から right までの全要素がピボット以下であり、
            // right+1 から max までの全要素はピボット以上である
            if( i > right ){
                // ピボット挿入位置のセット
                pibot_pos = right ;
                break ;
            }

            // 見つかれば調査区間を縮小する
            left = i ;
                // ここでも、min+1 から left-1 までの全要素はピボット
                // 以下であることが保証される

            // ピボットよりも小さな要素を右から探す
            for( int j = right ; j >= left ; j-- ){
                if( vect[j] < pibot ){
                    break ;
                }
            }

            // 小さな要素が見つからない：
            // つまり、min+1から left-1 までの全要素はピボット以下であり、
            // left から max までの全要素はピボット以上である
            if( j < left ){
                // ピボット挿入位置のセット
                pibot_pos = left-1 ;
                    // この特別な場合として、pipot_pos == min
                    // の場合がある
                break ;
            }

            // 見つかれば調査区間を縮小する
            right = j ;
                // ここでもまた、right+1 から max までの全要素は
                // ピボット以上であることが保証される

            // 大きな要素と小さな要素を入れ換える
            Type temp = vect[i] ;
            vect[i] = vect[j] ;
            vect[j] = temp ;

            // 区間をさらに縮小する
            left++ ;
            right-- ;
        }

        // ピボットを適切な位置に挿入する
        Type temp = vect[pibot_pos];
        vect[pibot_pos] = pibot ;
        vect[min] = temp ;

        // この段階で、区間 [min, pibot_pos-1]／[pibot_pos+1, max] の全要素は、
        // それぞれピボット以上／以下となっている。これら小区間に quick_sort
        // 関数を再帰的に適用する

        // 再帰
        quick_sort( min, pibot_pos - 1 ) ;
        quick_sort( pibot_pos + 1, max ) ;
    }
};

#endif //SORTABLEVECTOR_H
    アンダーラインを付した箇所は２ヶ所です。１ヶ所めは、継承の仕方「Vectorクラスを継承 してSortableVectorを定義している部分」で、２つめがメンバーイニシャライザを使用して、 親クラスのコンストラクタを呼び出しているところです。SortableVectorクラスのコンストラクタ は何もしていません。

    なお、デストラクタは記述していませんが、この場合も自動的に親クラスのデストラクタが 呼ばれます。

    継承しているところで、「public Vector」と記述していますが、このpublicは公開派生を 意味しています。これについては後述します。


6章：継承

3. 継承とキャスト

    よくわからないまま、SortableVectorクラスを作ってしまった感じがあると思います。 SortableVectorクラスは、Vectorクラスに「sort」関数を追加しただけなので、それほど 知識は必要としません。しかし、継承の恩恵もあまりありません。ここから先は、 継承の本質に迫ります。

// 親クラス
class Base{
public:
    void member_func_base();
};

// 子クラス
class Deriv : public Base{
public:
    void member_func_deriv();
};

// グローバル関数
void global_func_Base( Base b );
void global_func_pBase( Base* p );
void global_func_rBase( Base& r );

void main(){
    Base  base;
    Base* pBase;

    Deriv  deriv;
    Deriv* pDeriv;

    // 子クラスのポインタは親クラスのポインタへキャスト可能
    // ( Deriv* → Base* への変換可能)
    pBase = &deriv;

    // 親クラスのポインタは子クラスのポインタへキャストはダメ!!
    // ( Base* → *Deriv への変換はダメ) : コンパイルエラー
    pDeriv = &base;

    // 親クラスのポインタは子クラスのポインタへキャストは明示的にキャストすればOK
    // ( Base* → *Deriv への変換は明示的にキャストすればOK)
    // でも危険、する必要も普通はない
    pDeriv = (Deriv*)&base;

    // 当然OK
    global_func_Base( base );

    // 子クラスのインスタンスは親クラスへ変換可能
    // ( Deriv → Base への変換可能)
    global_func_Base( deriv );

    // 当然OK
    global_func_pBase( pBase );

    // 子クラスのポインタは親クラスのポインタへキャスト可能
    // ( Deriv* → Base* への変換可能)
    global_func_pBase( pDeriv );

    // 当然OK
    global_func_rBase( base );

    // 子クラスの参照は親クラスへ変換可能
    // ( Deriv& → Base& への変換可能)
    global_func_rBase( deriv );

    // 下の４つは当然大丈夫
    base .member_fubc_base();
    deriv.member_func_deriv();
    pBase ->member_func_base();
    pDeriv->member_func_deriv();

    // DerivクラスはBaseの性質(メンバー変数、メンバー関数)を
    // 引き継いでいるので下の２つは当然大丈夫
    deriv.member_func_base();
    pDeriv->member_func_base();

    // BaseクラスはDerivの性質(メンバー変数、メンバー関数)を
    // 引き継いでいないので下の２つは当然ダメ
    base.member_func_deriv();
    pBase->member_func_deriv();
}

    長くなっちゃいましたね。見るのもイヤって感じですね。

    つまり、まとめると親クラスは子クラスのように振る舞えないが、子クラスは親クラス のように振る舞うことができるということになります。

    私は「継承のことを進化する」と教えていると書きましたが、上のことを言い換えると 人間は犬の真似ができるが、犬は人間の真似ができないということになります。 (このたとえはマジでわかりやすいと思うんですが、いかがですか？)


6章：継承

4. スコープ

    同じ変数や関数名を、親クラス、子クラス、グローバルで定義するとどのように なるでしょうか。コンパイラは次のような順番で、目的とする変数や関数を検索します。

    派生クラスを作り、その派生クラスのメンバー関数での例を示します。
  • 派生クラスのメンバー関数内で定義した変数
  • 派生クラスのメンバー変数
  • 親クラスのメンバー変数
  • グローバルな変数
※ただしスコープ演算子「::」を指定した場合はこの限りではない

    実際のプログラムで例を示します。
// グローバルな変数
int a;
int b;
int c;

// 親クラス
class Base{
public:
    int a;
private:
    int b;
};

// 子クラス
class Deriv : public Base{
public:
    void member_func_deriv();
public:
    int b;
private:
    int c;
};

// 派生クラスの関数
void Deriv::member_func_deriv(){
    a = 1;          // 派生クラスにはないので、親クラスのaを参照
    b = 2;          // 派生クラスのbを参照
    c = 3;          // 派生クラスのcを参照

    ::a = 10;       // スコープ演算子があるので、グローバルなaを参照
    Base::b = 20;   // Baseクラスのbを参照しようとするが、privateなのでエラー

    int    a;
    double b;
    a = 100;        // 自分の関数内が優先される
    b = 1.1;        // 型が違っても大丈夫
}

void main(){
    Deriv d;

    d.a = 10;       // 派生クラスにはないので、親クラスのaを参照
    d.b = 10;       // 派生クラスのbを参照
    d.c = 10;       // 派生クラスのcを参照しようとするが、privateなのでエラー

    d.Base::a = 10; // スコープ演算子により、Baseクラスのaを参照
    d.Base::b = 10; // スコープ演算子により、Baseクラスのbを参照しようとするがprivateなのでエラー
    d.Base::c = 10; // Baseクラスにはcがないのでエラー

    a = 0;          // グローバルなaを参照
}


6章：継承

5. クラスの作成・破壊

    派生クラスも、中に親クラスを含んでいると考えられるので、派生クラスの作成・破壊は 包含クラスの場合のクラスの作成・破壊と基本的に同じである。

クラスの構築
  • 領域が確保される
  • 派生クラスの対応するメンバーイニシャライザを調べる(親クラスのコンストラクタがどのように呼ばれるか)
  • 親クラスの対応するメンバーイニシャライザを調べる(親クラスのメンバー変数がどのように初期化されるか)
  • 親クラスのメンバー変数が構築される(上で調べたメンバーイニシャライザが使われる)
  • 親クラスの対応するコンストラクタが呼ばれる
  • 派生クラスのメンバー変数が構築される(上で調べたメンバーイニシャライザが使われる)
  • 派生クラスの対応するコンストラクタが呼ばれる
※親クラスが他のクラスの派生クラスならば、再帰的に繰り返される

クラスの破壊
  • 派生クラスのデストラクタが呼ばれる
  • 派生クラスのメンバー変数が破壊される
  • 親クラスのデストラクタが呼ばれる
  • 親クラスのメンバー変数が破壊される
  • 領域が確保される
※親クラスが他のクラスの派生クラスならば、再帰的に繰り返される

    実際のプログラムで例を示します。
#include<iostream.h>

// 親クラスに包含されるクラス
class InBase{
public:
    // コンストラクタ
    InBase(int i){
        cout << "Construcotr of InBase class" << endl;
    }
    // デストラクタ
    ~InBase(){
        cout << "Destructor of InBase class" << endl;
    }
};

// 親クラス
class Base{
public:
    InBase inBase;

    // コンストラクタ
    Base(int i) : inBase(i){
        cout << "Construcotr of Base class" << endl;
    }
    // デストラクタ
    ~Base(){
        cout << "Destructor of Base class" << endl;
    }
};

// 子クラスに包含されるクラス
class InDeriv{
public:
    // コンストラクタ
    InDeriv(int i){
        cout << "Construcotr of InDeriv class" << endl;
    }
    // デストラクタ
    ~InDeriv(){
        cout << "Destructor of InDeriv class" << endl;
    }
};

// 子クラス
class Deriv : public Base{
public:
    InDeriv inDeriv;

    // コンストラクタ
    Deriv(int i) : Base(i), inDeriv(i){
        cout << "Construcotr of Deriv class" << endl;
    }
    // デストラクタ
    ~Deriv(){
        cout << "Destructor of Deriv class" << endl;
    }
};

void main(){
    cout << "main start" << endl;
    Deriv d(1);
    cout << "main end" << endl;
}

main start
Construcotr of InBase class
Construcotr of Base class
Construcotr of InDeriv class
Construcotr of Deriv class
main end
Destructor of Deriv class
Destructor of InDeriv class
Destructor of Base class
Destructor of InBase class

    やはりここでも派生クラスを進化したクラスということができる。人間でも受精したばかりは 手足はもとより、肺などの内臓もない(つまり退化した)状態から、人間へと成長していく。 クラスの構築についても退化した状態から進化した状態、つまり基底クラスから 派生クラスへと構築される。


6章：継承

6. 派生の種類

派生クラスを作る際に、
class Deriv : public Base
と記述しました。このpublicについて説明します。

    publicを記述すると「公開派生」と言います。当然ながらprotectedやprivateな 派生もあります。

    公開派生を使うと、派生クラスでは親クラスのpublicな変数・関数と親クラスの protectedな変数・関数にアクセスでき、親クラスのprivateな変数・関数にはアクセス できませんでした。他のクラスやmainなどで派生クラスのインスタンスを作って、 そのインスタンスを経由した場合は、親クラスのpublicな変数・関数、派生クラスの publicな変数・関数にしかアクセスできませんでした。

// 親クラス
class Base{
public:
    int base_public;
protected:
    int base_protected;
private:
    int base_private;
};

// 子クラス
class Deriv : public Base{
public:
    int deriv_public;
protected:
    int deriv_protected;
private:
    int deriv_private;

public:
    void deriv_func(){
        // 自分のクラスなので当然アクセスできる
        deriv_public    = 10;
        deriv_protected = 10;
        deriv_private   = 10;

        // 親クラスの場合は・・・
        base_public    = 10;    // publicはだれでもOK
        base_protected = 10;    // protectedは派生クラスからならOK
        base_private   = 10;    // privateは派生クラスでもエラー
    }
};

void main(){
    Deriv d;

    // 外部からは・・・
    d.deriv_public    = 10;     // publicはだれでもOK
    d.deriv_protected = 10;     // protectedは外部からはエラー
    d.deriv_private   = 10;     // privateは自分以外エラー

    d.base_public    = 10;      // publicはだれでもOK
    d.base_protected = 10;      // protectedは外部からはエラー
    d.base_private   = 10;      // privateは自分以外エラー
}

これをまとめると以下のようになります。
アクセス元 アクセス対象 可否
派生クラスのメンバー関数 基底クラスのpublic変数
基底クラスのprotected変数
基底クラスのprivate変数 		○
○
×
派生クラス外 基底クラスのpublic変数
基底クラスのprotected変数
基底クラスのprivate変数 		○
×
×

    では、public派生以外の場合はどうなるでしょうか・・・。protected派生では、 publicなメンバーもprotectedになってしまいます。private派生では、親クラスの全ての メンバーが派生クラスではprivateになってしまいます。

// 子クラス
class Deriv : protected Base{
public:
    int deriv_public;
protected:
    int deriv_protected;
private:
    int deriv_private;

public:
    void deriv_func(){
        // 親クラスの場合は・・・
        base_public    = 10;    // OK
        base_protected = 10;    // OK
        base_private   = 10;    // エラー
    }
};

// さらに子クラス
class D_Deriv : public Deriv{
    void d_deriv_func(){
        base_public    = 10;   // OK
        base_protected = 10;   // OK
        base_private   = 10;   // エラー
    }
};

void main(){
    D_Deriv d;

    d.base_public    = 10;      // エラー
    d.base_protected = 10;      // エラー
    d.base_private   = 10;      // エラー

    d.deriv_public    = 10;     // エラー
    d.deriv_protected = 10;     // エラー
    d.deriv_private   = 10;     // エラー
}

// 子クラス
class Deriv : private Base{
public:
    int deriv_public;
protected:
    int deriv_protected;
private:
    int deriv_private;

public:
    void deriv_func(){
        // 親クラスの場合は・・・
        base_public    = 10;    // OK
        base_protected = 10;    // OK
        base_private   = 10;    // エラー
    }
};

// さらに子クラス
class D_Deriv : public Deriv{
    void d_deriv_func(){
        base_public    = 10;   // エラー
        base_protected = 10;   // エラー
        base_private   = 10;   // エラー
    }
};

void main(){
    D_Deriv d;

    d.base_public    = 10;      // エラー
    d.base_protected = 10;      // エラー
    d.base_private   = 10;      // エラー

    d.deriv_public    = 10;     // エラー
    d.deriv_protected = 10;     // エラー
    d.deriv_private   = 10;     // エラー
}


6章：継承

7. 仮想関数

    基底クラスの関数を派生クラスで書き換える(横取りする、上書きする)ことをオーバーライド と言う。つまり基底クラスのある関数と同じ名前、同じ引数、同じ戻り値で、中身の違う 関数を派生クラスで定義することを言う。仮想関数とは、オーバーライドをするための メカニズムである。つまり基底クラスの動作または機能をあとから作成した派生クラスに よってカスタマイズする手段を提供する。

    メンバー関数の定義中に、戻り値の型の前に「virtual」というキーワードを付することにより、 その関数は仮想関数となる。以下に簡単な例を示す。

仮想関数 非仮想関数
// 親クラス
class Base{
public:
    virtual void func(){
        cout << "Base::func" << endl;
    }
};

// 子クラス
class Deriv : public Base{
public:
    virtual void func(){
        cout << "Deriv::func" << endl;
    }
};

void main(){
    Base  base;
    Deriv deriv;

    base.func();
    deriv.func();
}

// 親クラス
class Base{
public:
    void func(){
        cout << "Base::func" << endl;
    }
};

// 子クラス
class Deriv : public Base{
public:
    void func(){
        cout << "Deriv::func" << endl;
    }
};

void main(){
    Base  base;
    Deriv deriv;

    base.func();
    deriv.func();
}

Base::func
Deriv::func

Base::func
Deriv::func

    上記の例は簡単な例で、インスタンスを通じて関数を呼び出した場合である。 インスタンスを通じて関数を呼び出す場合、「virtual」を付けても付けなくても 動作は変わらない。

    仮想関数の効果が発揮されるのは、ポインタを通じて関数を呼び出す場合や、 参照を通じて関数を呼び出す場合である。
仮想関数 非仮想関数
// 親クラス
class Base{
public:
    virtual void func(){
        cout << "Base::func" << endl;
    }
};

// 子クラス
class Deriv : public Base{
public:
    virtual void func(){
        cout << "Deriv::func" << endl;
    }
};

void main(){
    Base*  pBase1 = new Base();
    Base*  pBase2 = new Deriv();
    Deriv* pDeriv = new Deriv();

    pBase1->func();
    pDeriv->func();
    pBase2->func();

    delete pBase1;
    delete pBase2;
    delete pDeriv;
}

// 親クラス
class Base{
public:
    void func(){
        cout << "Base::func" << endl;
    }
};

// 子クラス
class Deriv : public Base{
public:
    void func(){
        cout << "Deriv::func" << endl;
    }
};

void main(){
    Base*  pBase1 = new Base();
    Base*  pBase2 = new Deriv();
    Deriv* pDeriv = new Deriv();

    pBase1->func();
    pDeriv->func();
    pBase2->func();

    delete pBase1;
    delete pBase2;
    delete pDeriv;
}

Base::func
Deriv::func
Deriv::func

Base::func
Deriv::func
Base::func

    基底クラスであるBaseポインタ型に、派生クラスであるDerivをnewした場合(pBase2)を 見てください。非仮想関数の場合は、Baseクラスのfunc関数が呼ばれています。これは ポインタがBaseクラスのポインタだからです。(その中身がBaseクラスだろうが、Derivクラス だろうが関係なくポインタの型であるBaseのfunc関数が呼ばれます。)

    それに対して仮想関数では、ポインタが基底クラスであるBaseクラスののポインタである にも関らず、その中身が派生クラスであるので、Derivクラスのfunc関数が呼び出されています。

    非仮想関数では、実態が派生クラスだろうが基底クラスだろうが、その入れ物を示す ポインタや参照の型を見て、コンパイラがどちらのクラスの関数を呼び出すかを決めます。 これを静的結合といいます。

    これに対して仮想関数では、コンパイラがどちらのクラスの関数を呼び出すのか決める のではなく、実行時にポインタや参照が、その実態は派生クラスなのか基底クラスなのかを調べ、 動的にどちらの関数を呼び出すかを決定します。これを動的結合といいます。なお、 動的結合のメカニズムは後述します。

// 親クラス
class Base{
public:
    virtual void func(){
        cout << "Base::func" << endl;
    }
    virtual void func2(){
        cout << "Base::func2" << endl;
    }
};

// 子クラス
class Deriv : public Base{
public:
    virtual void func(){
        cout << "Deriv::func" << endl;
    }
    virtual void func2(){
        Base::func2();     // 親クラスのfunc2を呼び出す
        cout << "Base::func2" << endl;
    }
};

void main(){
    Base*  pBase = new Deriv();

    base->func();
    base->Base::func();

    cout << endl;
    base->func2();

    delete pBase;
}

Deriv::func
Base::func

Base::func2
Deriv::func2

    また、派生クラスの仮想関数から親クラスの関数を呼び出すことも可能である。さらに、 スコープ演算子「::」を利用することにより、明示的に基底クラスの関数を呼び出すことも 可能である。(この場合は静的結合になる。)

    なお、基底クラスの関数が仮想関数の場合、その派生クラスでオーバーライドする 関数に「virtual」を付けなくても、自動的にその関数も仮想関数となる(上記の例では Derivクラスのfunc関数)。しかし、一般的に省略せずに、派生クラスでも「virtual」を 付ける場合が多い。


6章：継承

8. 純粋仮想関数

// 親クラス
class Base{
public:
    virtual void func() = 0;
};

// 子クラス
class Deriv : public Base{
public:
    virtual void func(){
        cout << "Deriv::func" << endl;
    }
};

void main(){
    Base*  pBase1 = new Deriv();  // OK
    Base*  pBase2 = new Base();   // エラー

    Base  base;                   // エラー
    Deriv deriv;                  // OK

    delete pBase1;
    delete pBase2;
}

    上記の例のように仮想関数の末尾に「=0」と付けた関数は純粋仮想関数と呼ぶ。 純粋仮想関数を１つでも含むクラスは抽象クラスまたはアブストラクトクラス よ呼び、インスタンスを作ることができない。

    基底クラスに純粋仮想関数が複数ある場合、すべての純粋仮想関数を派生クラスで実装しなければ その派生クラスもまた抽象クラスになる。

// 親クラス
class Base{
public:
    virtual void func1() = 0;
    virtual void func2() = 0;
};

// 子クラス(まだ抽象クラス)
class Deriv : public Base{
public:
    virtual void func1(){
        cout << "Deriv::func1" << endl;
    }
};

// さらに子クラス
class DDeriv : public Deriv{
public:
    virtual void func2(){
        cout << "DDeriv::func2" << endl;
    }
};
void main(){
    Base   base;                   // エラー
    Deriv  deriv;                  // エラー
    DDeriv dderiv;                 // OK
}

    抽象クラスは概念として導入する場合が多いです。つまりお絵かきソフトを作ろうと 考えた場合、図形データはVectorクラスに入れます。 図形データは以下のように作ります。
// 図形クラス(抽象クラス)
class Zukei{
protected:
    int x1, x2, y1, y2;
public:
    // 描画する関数
    virtual void draw() = 0;

    // セーブする関数
    virtual ostream& save(ostream& os) = 0;
};

// 四角形クラス
class Rectangle : public Zukei{
public:
    // 描画する関数(このクラスで実装する)
    virtual void draw();

    // セーブする関数(このクラスで実装する)
    virtual ostream& save(ostream& os);
};

// 直線クラス
class Line : public Zukei{
public:
    // 描画する関数(このクラスで実装する)
    virtual void draw();

    // セーブする関数(このクラスで実装する)
    virtual ostream& save(ostream& os);
};

// 円クラス
class Circle : public Zukei{
public:
    // 描画する関数(このクラスで実装する)
    virtual void draw();

    // セーブする関数(このクラスで実装する)
    virtual ostream& save(ostream& os);
};

    図形を描画したい場合は、Vectorクラスのすべてのデータに対してdraw関数を呼べば 良いだけです。セーブしたいときもVectorクラスの全てのデータに対してsave関数を呼べば 良いだけです。抽象クラスはこのように、いくつかのデータクラスなどの親分役として作ります。


6章：継承

9. 仮想デストラクタ

    派生クラスを作成した場合、コンストラクタは、基底クラスのコンストラクタ、 派生クラスのコンストラクタの順に呼ばれることは前述 の通りである。

    そしてデストラクタは、派生クラスのデストラクタ、基底クラスのデストラクタの 順に呼ばれることも述べた。

    しかし、デストラクタが仮想でないと、基底クラスポインタに、派生クラスのインスタンスの ポインタが代入されている場合、deleteしても、それは静的結合に よってデストラクタが呼ばれるため、基底クラスのデストラクタしか呼ばれない。

    下の例では、デストラクタが非仮想の場合と仮想の場合を比較した。基底クラスのポインタに 派生クラスをnewして、deleteしたときに違いが現れることを示している。

非仮想デストラクタ 仮想デストラクタ
// 親クラス
class Base{
public:
    Base(){
        cout << "Base::Base()" << endl;
    }
    ~Base(){
        cout << "Base::~Base()" << endl;
    }
};

// 子クラス
class Deriv : public Base{
public:
    Deriv(){
        cout << "Deriv::Deriv()" << endl;
    }
    ~Deriv(){
        cout << "Deriv::~Deriv()" << endl;
    }
};

void main(){
    {
        Deriv deriv;
    }
    cout << endl;

    Base*  pBase;
    Deriv* pDeriv;

    pBase = new Base();
    delete pBase;

    cout << endl;

    pBase = new Deriv();
    delete pBase;

    cout << endl;

    pDeriv = new Deriv();
    delete pDeriv;
}

// 親クラス
class Base{
public:
    Base(){
        cout << "Base::Base()" << endl;
    }
    virtual ~Base(){
        cout << "Base::~Base()" << endl;
    }
};

// 子クラス
class Deriv : public Base{
public:
    Deriv(){
        cout << "Deriv::Deriv()" << endl;
    }
    virtual ~Deriv(){
        cout << "Deriv::~Deriv()" << endl;
    }
};

void main(){
    {
        Deriv deriv;
    }
    cout << endl;

    Base*  pBase;
    Deriv* pDeriv;

    pBase = new Base();
    delete pBase;

    cout << endl;

    pBase = new Deriv();
    delete pBase;

    cout << endl;

    pDeriv = new Deriv();
    delete pDeriv;
}

Base::Base()
Deriv::Deriv()
Derive::Deriv()
Base::Base()

Base::Base()
Base::Base()

Base::Base()
Deriv::Deriv()
Base::Base()

Base::Base()
Deriv::Deriv()
Derive::Deriv()
Base::Base()

Base::Base()
Deriv::Deriv()
Deriv::~Deriv()
Base::~Base()

Base::Base()
Base::~Base()

Base::Base()
Deriv::Deriv()
Deriv::~Deriv()
Base::~Base()

Base::Base()
Deriv::Deriv()
Deriv::~Deriv()
Base::~Base()

    このように派生クラスのインスタンスを作っているにもかかわらず、派生クラスのデストラクタが 呼ばれないのは問題がある。したがって、今後、クラスが派生される可能性が少しでもあるばらば、 デストラクタは仮想とするべきである。


4章：クラス

10. 例題）例外クラス

    例外処理は、int型などのデフォルトで定義されている型以外 にも、自分で作ったクラスをthrowすることができる。これを利用して、Exceptionクラスを 基底クラスとし、いくつかの例外を定義します。

Exception.h

#ifndef EXCEPTION_H
#define EXCEPTION_H

#include<iostream.h>
#include<string.h>

class Exception{
    friend ostream& operator<<(ostream& os, const Exception& e){
        return os << e.m_strExceptionName << " in " << e.m_strSourceFile << " : " << e.m_strLineNumer << endl;
    }

protected:
    const char* m_strSourceFile;
    int         m_strLineNumer;
    const char* m_strExceptionName;

protected:
    // コンストラクタ
    Exception(const char* SourceFile, const int LineNumber):
        m_strSourceFile(SourceFile),
        m_strLineNumer(LineNumber)
    {
    }

    // 例外名のセット
    void SetExceptionName(const char* strExceptionName){
        m_strExceptionName = strExceptionName;
    }
};

// 0で割ったときの例外
class DivideByZeroException : public Exception{
public:
    // コンストラクタ
    DivideByZeroException(const char* SourceFile, const int LineNumber):
        Exception(SourceFile, LineNumber)
    {
        SetExceptionName( "DivideByZeroException" );
    }
};

// 添え字が無効
class ArrayIndexOutOfBoundsException : public Exception{
public:
    // コンストラクタ
    ArrayIndexOutOfBoundsException(const char* SourceFile, const int LineNumber):
        Exception(SourceFile, LineNumber)
    {
        SetExceptionName( "ArrayIndexOutOfBoundsException" );
    }
};

#define THROW(_e_)                  \
    {                               \
        _e_ e(__FILE__, __LINE__);  \
        throw e;                    \
    }
#endif // ECEPTION_H

Vector.h

#ifndef  _VECTOR_H_
#define  _VECTOR_H_

#include<iostream.h>
#include"exception.h"

template<class Type>
class Vector{
protected:
    void copy( const Vector<Type>& src ){
        size  = src.size;
        vect = new Type[size];
        for( int i = 0 ; i < size ; i++ )
            vect[i] = src.vect[i];
    }

    // 配列サイズ
    int size;

    // 配列本体
    Type *vect;

public:
    Vector( int sz = 100 ){
        size = sz ;

        vect = new Type[size] ;
    }

    Vector( const Vector<Type>& src ){
        copy( src ) ;
    }

    virtual ~Vector(){
        delete (vect) ;
    }

    Type& operator[](int index){
        // 添え字のチェック
        if( index < 0 || index >= size ){
            // 無効な添え字の時の例外を投げる
            THROW(ArrayIndexOutOfBoundsException);
        }
        return *(vect + index);
    }

    int get_length()const{
        return size;
    }

    Vector<Type>& operator=( const Vector<Type>& src ){
        if( &src != this ){
            delete [] vect;

            copy( src );
        }
        return *this;
    }
};

#endif //_VECTOR_H_

#include<iostream.h>
#include"exception.h"
#include"Vector.h"

// 割り算関数
// もしかすると例外を投げる可能性があるかもしれない
int divide(int n1, int n2){
    if( n2 == 0 )
        // 0で割ったときの例外を投げる
        THROW(DivideByZeroException);

    return n1 / n2;
}

void main(){
    int n1, n2, n3 = 0 ;
    cin >> n1 >> n2;

    // 例外が起こるかもしれないブロック
    try{
        Vector<int>    vect(5);
        vect[10] = 10;
    }
    // 例外(ArrayIndexOutOfBoundsException)をキャッチ
    catch(ArrayIndexOutOfBoundsException e){
        cerr << e << endl;
    }

    // 例外が起こるかもしれないブロック
    try{
        n3 = divide(n1, n2);
        cout << n1 << " / " << n2 << " = " << n3 << endl;
    }
    // 例外(基底クラスなので、Exceptionクラスの派生クラスすべて)をキャッチ
    catch( Exception e ){
        cerr << e << endl;
    }
}

1 0
ArrayIndexOutOfBoundsException in d:\my document\cpp\vector.h : 42

DivideByZeroException in d:\my document\cpp\cpptest.cpp : 10


4章：クラス

11. V-Table(VF-Table)

    ここでは、仮想関数の動的結合のメカニズムについて 説明します。V-Tableについて知らなくてもプログラムは書けますので、読み飛ばしていただいても 何の問題もありません。

#include <iostream.h>

class Base{
public:
    long l;
    virtual void func1(){
        cout << "Base:func1()" << endl;
    }
};

class Deriv : public Base{
public:
    virtual void func1(){
        cout << "Deriv:func1()" << endl;
    }
};

void main(){
    Base* p = new Deriv();
    p->func1();
}

Deriv:func1()

    上記のようなプログラムでpは、Baseクラス型ポインタであるのに、その ポインタが指しているインスタンスはDerivクラスです。したがってDerivクラスの func1が呼ばれます。これを動的結合といいました。

    動的結合はインスタンスの中身を調べるために、変数の型(この場合Baseクラスのポインタ型) では当然判断できません。ということは、インスタンスの実態に何か細工がしているはずです。 そこで、仮想関数を使った場合とそうでない場合のインスタンスのサイズを見てみましょう。

非仮想関数 仮想関数
#include <iostream.h>

class A{
public:
    long l;
    void func1(){
        cout << "A:func1()" << endl;
    }
};

void main(){
        cout << sizeof(A) << endl;
}

#include <iostream.h>

class A{
public:
    long l;
    virtual void func1(){
        cout << "A:func1()" << endl;
    }
};

void main(){
        cout << sizeof(A) << endl;
}

4

8

    左と右では仮想関数か、そうでないかの違いしかないのに、サイズが４バイト異なっています。 この４バイトが、上で述べた「細工」なのです。この４バイトはV-Tableへのポインタです。

    では、V-Tableとは何なのでしょうか。V-Tableとプログラムが起動する順番などを 説明します。

#include <iostream.h>

class Base{
public:
    long l;
    virtualvoid v_func1(){
        :
        :
    }

    virtualvoid v_func2(){
        :
        :
    }

    void func3(){
        :
        :
    }
};

class Deriv : public Base{
public:
    virtual void func1(){
        :
        :
    }
};

void main(){
    Base*p = new Deriv();
    p->func1();
    p->func2();
}

    上記のようなプログラムがあったとします。まず、このプログラムを実行しようとすると、 プログラム領域が適当なアドレスに割り当てられます。ここでは仮に下図のように、 「Base::v_func1()」が1000番地、「Base::v_func2()」が2000番地・・・、に配置されたとします。

    次にV-Tableが作られます。V-Tableは各クラスの仮想関数と実際に配置されたアドレスとの 対応表のようなものです。ここでは仮に下図のように、Baseクラス用のV-Tableが10000番地に、 Derivクラス用のV-Tableは11000番地に配置されたとします。V-Tableには非仮想関数は含まれません。 また、V-Tableはクラスに１つで、そのインスタンスが複数作られてもV-Tableは１つしか 存在しません。

    ここまではプログラムがOSによってロードされた時点で行われ、実行開始前のできごとです。

    さて、いよいよ実行です。仮にDerivクラスのインスタンスが22000番地に作成されたとします。 Derivクラスのメンバー変数はlong型のl１つです。それに加えて、Derivクラス用のV-Table を指すポインタが含まれるので、sizeof(Deriv)は計８バイトになります。

    ここで、「p->v_func2()」を実行したとします。まず、V-Tableを参照して、
v_func2()が2000番地から始まっていることを知ります。そして2000番地にジャンプすることにより
「Base::v_func2()」が実行されます。

    次に、「p->v_func1()」を実行したとします。まず、V-Tableを参照して、
v_func1()が4000番地から始まっていることを知ります。そして4000番地にジャンプすることにより
「Deriv::v_func1()」が実行されます。

    では、「p->func3()」を実行した場合はどうでしょうか。func3は仮想関数 ではないので、動的結合ではなく、コンパイル時にどの関数を実行するかを決定する静的結合 になります。したがって、V-Tableを参照することなく、3000番地すなわち「Base::func3()」 にジャンプするようにコンパイルされます。


4章：クラス

12. 例題）お絵かきソフト

    お絵かきソフトを作ります。メニューバーから直線、四角形、円を選択でき、 マウスの左ボタンを押した場所から放した場所までの領域に描画します。

    座標を表すためのPointクラス(メンバー変数はint型のx座標とy座標)を用意します。 図形データとして、抽象クラスであるDiagramクラスを用意します。メンバー変数は、 開始座標と終了座標です。また、純粋仮想関数として描画関数(Draw)を定義しています。 つまり、Diagramの派生クラスは、Draw関数を必ず実装する必要があります。そして、 Diagramクラスを派生した具象クラスとして、直線(Line)クラス、四角形(Rect)クラス、 円(Circle)クラスを定義します。これら３つの違いは、Draw関数が異なるだけです。

    CDrawDocクラスに、データを格納するためのlistクラスのインスタンスを 持っています。マウスを操作し、データが確定すると、このリストにLineクラスや Rectクラス、Circleクラスのインスタンスのポインタを格納していきます。

    画面に描画したい場合は、このリストすべてのインスタンスに対して、 Draw関数を呼び出しています。

    また、このソフトはUndo機能と、すべてをクリアする機能を持ちます。Undoは リストの一番最後のデータを削除して、表示しなおすだけです。すべてクリアしたい 場合は、リストをからにすればよいのです。

    コンパイラはVisual C++が必要です。また、ZIPで圧縮していますので、解凍ソフトも 必要です。ダウンロードはこちらです。(約10KBytes)

Diagram.h

// Diagram.h: Diagram クラスのインターフェイス
//
//////////////////////////////////////////////////////////////////////

#ifndef DIAGRAM_H
#define DIAGRAM_H

class Point{
public:
    int x, y;

    Point(int x=0, int y=0){
        set(x, y);
    }

    void set(int x, int y){
        this->x = x;
        this->y = y;
    }
};

class Diagram{
protected:
    Point    pntStart;
    Point    pntEnd;
public:
    Diagram();
    virtual ~Diagram();

    // 描画する
    virtual void Draw(CDC* pDC) = 0;

    virtual void SetStartPoint(Point pnt){
        pntStart = pnt;
    }
    virtual void SetEndPoint(Point pnt){
        pntEnd = pnt;
    }
};

//
// 四角形
class Rect : public Diagram{
public:
    Rect();
    virtual ~Rect();
    virtual void Draw(CDC* pDC);
};

//
// 直線
class Line : public Diagram{
public:
    Line();
    virtual ~Line();
    virtual void Draw(CDC* pDC);
};

//
// 円
class Circle : public Diagram{
public:
    Circle();
    virtual ~Circle();
    virtual void Draw(CDC* pDC);
};

#endif // DIAGRAM_H

Diagram.cpp

// Diagram.cpp: Diagram クラスのインプリメンテーション
//
//////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "Draw.h"
#include "Diagram.h"

//////////////////////////////////////////////////////////////////////
// 構築/消滅
//////////////////////////////////////////////////////////////////////

Diagram::Diagram(){
}

Diagram::~Diagram(){
}
/////////////////////////////////////////////
//
// 四角形
//
/////////////////////////////////////////////
Rect::Rect() : Diagram(){
}

Rect::~Rect(){
}

void Rect::Draw(CDC* pDC){
    pDC->MoveTo( pntStart.x, pntStart.y );
    pDC->LineTo( pntStart.x, pntEnd.y   );
    pDC->LineTo( pntEnd.x,   pntEnd.y   );
    pDC->LineTo( pntEnd.x,   pntStart.y );
    pDC->LineTo( pntStart.x, pntStart.y );
}
/////////////////////////////////////////////
//
// 直線
//
/////////////////////////////////////////////
Line::Line() : Diagram(){
}

Line::~Line(){
}

void Line::Draw(CDC* pDC){
    pDC->MoveTo( pntStart.x, pntStart.y );
    pDC->LineTo( pntEnd.x,   pntEnd.y   );
}
/////////////////////////////////////////////
//
// 円
//
/////////////////////////////////////////////
Circle::Circle() : Diagram(){
}

Circle::~Circle(){
}

void Circle::Draw(CDC* pDC){
    pDC->Ellipse( pntStart.x, pntStart.y, pntEnd.x, pntEnd.y );
}


4章：クラス

13. 継承と包含

新しいクラスを作成しようとした際には、次の３つの選択がある。
  1. まったくゼロから作る
  2. 既存のクラスを継承して、派生クラスとして作る
  3. 既存のクラスを包含して、新しく作る

    まったく新しい機能のクラスを作る場合は(1)のゼロから作る方法ですが、問題は (2)の継承か、(3)の包含かは、結構迷う場合もあります。プログラマー(設計者)の 経験に頼るところが大きいのですが、迷った場合は以下の５つの質問をしてみて、 すべての項目がYESだった場合は「継承」にします。どれか１つでもNOがあれば 「包含」にします。

    なお、質問事項の他にその解説と、お絵かきソフトの 例を示しました。

  • 既存のクラスの一種か(... kind of ...か)？
    文字どおり、基底クラスの一種ではないものは、派生クラスにするべきではありません。

    RectクラスやLineクラスはDiagramクラスの一種であるため、この質問はYESになります。

  • 機能の削除ではなく、機能の追加か？
    派生クラスは機能を追加するべきで、機能を削除して廉価版を作るべき ではありません。もし使い方を単純にして低機能なクラスを作るのであれば、低機能なクラスを 基底クラスにするべきです。もしくは、包含クラスとして実装する方法もあります。

    抽象クラスを派生して具象クラスを作ろうとしているので、YESになります。

  • 派生系列に、矛盾がないか？
    今までは分類で派生してきたのに、新しく派生したものは機能で分類していないか。 例えば、哺乳類クラスを派生して、犬クラス、猫クラスを作ったのに、さらに犬クラスを派生して 盲導犬クラスや警察犬クラスを作ろうとしていないか。(盲導犬や警察犬は種類ではなく、機能の追加になる。)

    １世代しか継承しないので、この質問はYESになります。

  • 既存のクラスの派生クラスにした場合、基底クラスやたの派生クラスに変更を加える必要はないか？
    他のクラスを変更するということは、それまで作ってきたプログラムが使えなくなるという ことです。他のクラスに変更したり、他のクラスが無用になるくらいなら、新しく派生クラスを つくるべきではありません。

    Rectクラス、Lineクラスが存在し、新たにCIrcleクラスを付け加える場合、どのクラスにも変更がないので、 この質問はYESになります。

  • わけのわからない(無用な)機能の追加ではないか？
    全然関係ない機能を付け加えるべきではありません。

    Diagramクラスは抽象クラスで、Rectクラスなどは、その具象クラスなので、この質問もYESになります。


4章：クラス

    では次に、ソート機能付ベクタークラスと、 リングバッファーの例を示します。両クラスともベクタークラスを 利用して実装したクラスです。

  • 既存のクラスの一種か(... kind of ...か)？
RingBuffer・・・NOです。
    リングバッファーはベクターの一種ではありません。リングバッファーを作るのに、 ベクターの機能を利用しているだけです。
SortableVector・・・YESです。
    ソート機能付ベクターというくらいですから、ベクターの一種です。

  • 機能の削除ではなく、機能の追加か？
RingBuffer・・・NOです。
    機能の追加ではなく、別の用途に使われるクラスです。
SortableVector・・・YESです。
    ソート機能付ベクターというくらいですから、ソート機能を追加したくラスです。

  • 派生系列に、矛盾がないか？
RingBuffer・・・???です。
    他のクラスをクラスを考えていないので、系列も何もまだこの段階ではわかりませんね。
SortableVector・・・???です。
    他のクラスをクラスを考えていないので、系列も何もまだこの段階ではわかりませんね。

  • 既存のクラスの派生クラスにした場合、基底クラスやたの派生クラスに変更を加える必要はないか？
RingBuffer・・・YESかなぁ?
    ベクターを継承してリングバッファーを作ったとしても、基底クラスの変更は内容に思います。 強いて言えば、operator[]を外部から勝手に使えなくする必要があるので、public派生ではなく protected派生にして、リングバッファーを作る必要があります。protected派生はC++の仕様ですので 他の言語ではできません。これはできれば避けたいところですね。
SortableVector・・・YESです。
    ソート機能付ベクタークラスを作るうえで、基底クラスを変更する必要はありません。

  • わけのわからない(無用な)機能の追加ではないか？
RingBuffer・・・NOです。
    先程も書きましたとおり、機能の追加ではありません。
SortableVector・・・YESです。
    配列をソートしたいと思ったことはあると思います。ソート機能を内蔵したベクタークラスが あっても、別にわけのわからない機能ではありません。

    したがって、リングバッファーはベクターを包含して作成しましたし、ソート機能付ベクター クラスはベクタークラスを継承して作りました。


4章：クラス

14. 多重継承

    多重継承は、ひし形クラスと長方形クラスの両方の特徴を継承した正方形クラスのような ものを言います。

    多重継承するには以下のように記述します。
class 派生クラス名 : public 基本クラス名1, public 基本クラス名2....{
    :
    :
}
    基本クラスそれぞれにアクセス修飾子(public, protected, private)をつけることができます。

    次に、コンストラクタの呼び出し型を説明します。
class Deriv : public Base1, public Base2{
public:
    Deriv(....) :
        Base1(....),
        Base2(....){
        :
        :
    }
};

    ここまではそんなにむつかしいことは含まれていないと思います。

    複数の基本クラスに同じ名前のメンバー変数や、メンバー関数があった場合はどのように なるでしょう。その場合は、スコープ演算子「::」を使用しなければなりません。
// 基本クラス1
class Base1{
public:
    int    nBase;

    virtual void funcBase(int n){
        nBase = n;
    }
};

// 基本クラス2
class Base2{
public:
    int nBase;

    virtual void funcBase(int n){
        nBase = n;
    }
};

// 多重継承した派生クラス
class Deriv : public Base1, public Base2{
public:
    void funcDeriv(int n){
        funcBase(n);            // エラー
        Base1::funcBase(n);     // OK
    }
};

void main(){
    Deriv    d;
    d.nBase = 10;               // エラー
    d.Base1::nBase = 10;        // OK

    d.funcBase(10);             // エラー
    d.Base2::funcBase(10);      // OK
}


4章：クラス

15. 多重継承の用途

    多重継承は、ひし形クラスと長方形クラスの両方の特徴を継承した正方形クラスのような ものを言います。しかし四角形クラスから派生したひし形クラスと長方形クラスを多重継承して 正方形クラスを作るようなことはあまりありません。つまり１つの系列から派生した複数の クラスを多重継承することはあまりありません。

    多重継承はどのような使い方をするかといいますと、種類で派生した系列があります。 それとは全く関係なく機能で派生した系列があります。この２つを多重継承して、 １つの具象クラスを作るような場合です。

    例えば上図のように、種類の継承系列として、哺乳類クラスを継承して犬クラスや 猫クラスなどがあります。またそれとは別の系列として、機能クラスを継承して、 救助クラスや盲導クラスなどがあります。盲導犬クラスを作る場合は、盲導クラスと 犬クラスを多重継承して作ります。麻薬捜査犬クラスを作る場合は、犬クラスと麻薬捜査 クラスを多重継承します。

    もうひとつの使い方として、インターフェースとして使う場合があります。 おそらくこの方法が一番多く使われる方法です。

    インターフェースとは、上の例と似ていますが、機能を司るクラスのことを言います。 もう少し具体的に言うと、インターフェースは抽象クラスで、それを派生したクラスでは 絶対に実装してほしい関数を純粋仮想関数として定義します。

    上図はインターフェースの例です。基底クラスとしてダイアログクラスがあり、何かの 計算結果を表示するための結果表示ダイアログクラスはダイアログクラスを継承しています。 それとは別にインターフェースクラスがあり、これは純粋仮想関数として、更新という 関数が定義されています。つまりインタフェースクラスは抽象クラスです。そして インターフェースクラスと結果表示ダイアログを多重継承して、途中結果表示ダイアログ を作っています。

    また、Windowsでファイルコピーやアプリケーションのインストールの時にどのくらい 進んだかを表示するプログレスバーというのがありますが、それを表示するプログレスバー クラスとインターフェースを多重継承して、進行状況表示プログレスバークラスを定義しています。

    何かを計算しているときに、更新関数を呼びますが、これをC++で実装すると以下のように なります。

// ダイアログクラス
class Dialog{
public:
    Dialog();
    virtual ~Dialog();
    virtual void Show();
    virtual void Init();
};

// プログレスバークラス
class ProgressBar{
public:
    ProgressBar();
    virtual ~ProgressBar();
    virtual void Show();
    virtual void Init();
};

// インターフェースクラス
class Interface{
public:
    virtual void Renew() = 0;
};

// 結果表示ダイアログクラス
class ShowResultDialog : public Dialog{
public:
    ShowResultDialog() : Dialog();
    virtual ~ShowResultDialog();
    virtual void Show();
    virtual void Init();
};

// 途中結果表示ダイアログクラス
class ShowHalfwayResultDialog : public Dialog, public Interface{
public:
    ShowHalfwayResultDialog() : Dialog();
    virtual ~ShowHalfwayResultDialog();
    virtual void Show();
    virtual void Init();
    virtual void Renew();
};

// 進行状況表示プログレスバークラス
class ShowProgressBar : public ProgressBar, public Interface{
public:
    ShowProgressBar() : ProgressBar();
    virtual ~ShowProgressBar();
    virtual void Show();
    virtual void Init();
    virtual void Renew();
};

// 計算関数
// pInterface  進行状況を通知するクラスの配列
// n           進行状況を通知するクラスの配列の要素数
void Calculation(Interface* pInterface[], int n){
    for( int i = 0 ; i < 1000 ; i++ ){
        :
        :

        // 進行状況を通知
        for( int j = 0 ; j < n ; j++ )
            pInterface[j]->Renew();
    }
}

// メイン
void main(){
    ShowHalfwayResultDialog  dlg;
    ShowProgressBar          bar;

    Interface*               pInterface[2];

    pInterface[0] = &dlg;
    pInterface[1] = &bar;

    // 計算
    Calculation( pInterface, sizeof(pInterface) / sizeof(Interface*) );

    ShowResultDialog ResultDialog;
    ResultDialog.Show();
}


4章：クラス

16. 仮想クラス

    下図のように同じ基底クラス(Baseクラス)から派生したDeriv1クラスと、Deriv2クラスが あり、その両者を多重継承したDeriv3クラスを作りたいとします。

    しかし、下のように記述するとエラーになります。それは変数aは、Deriv1クラスの 基底クラスであるBaseクラスと、Deriv2クラスの基底クラスであるBaseクラスに 定義されているからです。つまり同じBaseクラスを２回継承しているからです。

class Base{
public:
    virtual void Test(){}
    int a;

};

class Deriv1 : public Base{
public:
    virtual void Test(){}
    int b;
};

class Deriv2 : public Base{
public:
    virtual void Test(){}
    int c;
};

class Deriv3 : public Deriv1, public Deriv2{
public:
    virtual void Test(){}
    int d;
};

void main(){
    Deriv3 d;
    d.a = 10;         // エラー
    d.Base::a = 10;   // エラー
}

    このような場合、Baseクラスは１回しか継承しないようにすることができます。
class Base{
public:
    virtual void Test(){}
    int a;

};

class Deriv1 : virtual public Base{
public:
    virtual void Test(){}
    int b;
};

class Deriv2 : virtual public Base{
public:
    virtual void Test(){}
    int c;
};

class Deriv3 : public Deriv1, public Deriv2{
public:
    virtual void Test(){}
    int d;
};

void main(){
    Deriv3 d;
    d.a = 10;
    d.Base::a = 10;
}

    上記のように「virtual」を付けると、Deriv3クラスはBaseクラスを１回しか継承しないように なります。


4章：クラス

17. 実行時型情報(RTTI)

    Baseクラスを派生したDeriv1クラスとDeriv2クラスがあるとします。Deriv1クラスの インスタンスをnewして、Baseクラスのポインタ型変数に代入したはいいけど、それが Deriv1クラスのインスタンスか、Deriv2クラスのインスタンスかを知りたい場合もあります。 このようにインスタンスがどのクラスのインスタンスかを調べなければいけないような 設計は良い設計とはいえません。しかし、調べられると便利な場合もあります。

    このように動的に型情報を取得する機構を「実行時型情報(RTTI=Runtime Type Information」 といいます。

    このRTTIを取得するクラスとして、「type_info」クラスがあります。このtype_infoクラスを 利用するには、「typeinfo」をインクルードする必要があります。

    type_infoクラスのインスタンスを得るには、以下のようにtypeid演算子を使用します。 戻り値は、const type_infoの参照です。
const type_info& typeid(expression)

例）
    int i;
    typeid(i);
    typeid(cout);

    そして、type_infoクラスには以下のような公開関数があります。 type_infoクラスの主要な関数
関数名 | 書　式 | 説　明
operator== | int operator==(const type_info& rhs)const | ２つのインスタンスを比較し、同じならば0以外が返ります。
operator!= | int operator!=(const type_info& rhs)const | ２つのインスタンスを比較し、異なるならば0以外が返ります。
before | int before(const type_info& rhs)const | 主に内部的に使用する関数なので、プログラマーにとってはあまり意味を持ちません。
name | const char* name()const | クラス名をNULL文字列として返します。
raw_name | const char* raw_name()const | 主に内部的に使用する関数なので、プログラマーにとってはあまり意味を持ちません。

    それでは簡単な例を紹介します。
#include<iostream.h>
#include<typeinfo>

class Base{
public:
    virtual void Test(){}
    int a;

};

class Deriv : public Base{
public:
    virtual void Test(){}
    int b;
};

void main(){
    Base* pBase = new Deriv();
    int i;

    // int型変数iの型は・・・
    cout << typeid(i).name() << endl;

    // 型を直接記述することもできる
    cout << typeid(char).name() << endl;

    // ポインタ型もOK
    // ただし、Deriv*型にならないことに注意
    cout << typeid(pBase).name() << endl;

    // Base*型のインスタンスの中身はDerivクラスのインスタンス
    cout << typeid(*pBase).name() << endl;

    // pBaseの中のインスタンスがDerivクラスなのでtrueと表示
    cout << (typeid(*pBase) == typeid(Deriv) ? "true" : "false") << endl;

    delete pBase;
}

int
char
class Base *
class Deriv
true

    仮想関数を含まないクラスでは静的結合になるため、RTTIも無効になります。
#include<iostream.h>
#include<typeinfo>

class Base{
public:
//  virtual void Test(){}
    int a;

};

class Deriv : public Base{
public:
//  virtual void Test(){}
    int b;
};

void main(){
    Base* pBase = new Deriv();
    int i;

    // ポインタ型もOK
    // ただし、Deriv*型にならないことに注意
    cout << typeid(pBase).name() << endl;

    // Base*型のインスタンスの中身はDerivクラスのインスタンス
    cout << typeid(*pBase).name() << endl;

    // pBaseの中のインスタンスがDerivクラスなのでtrueと表示
    cout << (typeid(*pBase) == typeid(Deriv) ? "true" : "false") << endl;

    delete pBase;
}

class Base *
class Base
false

    次に、テンプレートクラスの例を示します。
#include<iostream.h>
#include<typeinfo>

template<class Type>
class Base{
public:
    virtual void Test(){}
    Type a;

};

template<class Type>
class Deriv : public Base<Type>{
public:
    virtual void Test(){}
    Type b;
};

void main(){
    Deriv<int>   intD;
    Deriv<char>  charD;

    cout << "intD :" << typeid(intD) .name() << endl;
    cout << "charD:" << typeid(charD).name() << endl;
    cout << ( typeid(intD) == typeid(charD) ? "true" : "false" ) << endl;
}

intD :class Deriv<int>
charD:class Deriv<char>
false

    上記の例のように、テンプレートでは、その両方とも同じでないと、同じクラスとは 認識しません。


4章：クラス

18. dynamic_cast

    type_infoを用いると、どのクラスのインスタンスかを調べることができました。 しかし、実際には「派生クラスか」を調べたいこともあります。そのような場合、 type_infoを使用すると、各派生クラスすべてに対して調べなくてはいけないので 不便です。

    このような場合は、「dynamic_cast」演算子を使用します。 これは、キャスト可能ならばキャストし、キャストできなければNULLを代入するという 演算子です。

    それでは簡単な例を紹介します。
#include<iostream.h>
#include<typeinfo>

class Base{
public:
    virtual void Test(){}
    int a;

};

class Deriv1 : public Base{
public:
    virtual void Test(){}
    int b;
};

class Deriv2 : public Base{
public:
    virtual void Test(){}
    int b;
};

class Other{
public:
    virtual void Test(){}
    int c;
};

void main(){
    Base*   pBase;
    Deriv1* pDeriv1;
    Deriv2* pDeriv2;
    Other*  pOther;

    // Deriv1クラスのインスタンスが入っているBase* pBaseは Deriv1* pDeriv1にキャストできるか？
    pBase = new Deriv1();
    pDeriv1 = dynamic_cast<Deriv1*>(pBase);
    cout << (pDeriv1 ? "cast ok" : "cast fail") << endl;
    delete pBase;

    // Baseクラスのインスタンスが入っているBase* pBaseは Deriv1* pDeriv1にキャストできるか？
    pBase = new Base();
    pDeriv1 = dynamic_cast<Deriv1*>(pBase);
    cout << (pDeriv1 ? "cast ok" : "cast fail") << endl;
    delete pBase;

    // Deriv2クラスのインスタンスが入っているBase* pBaseは Deriv1* pDeriv1にキャストできるか？
    pBase = new Deriv2();
    pDeriv1 = dynamic_cast<Deriv1*>(pBase);
    cout << (pDeriv1 ? "cast ok" : "cast fail") << endl;
    delete pBase;

    // 関係のないインスタンスが入っているOther* pOtherは Base* pBaseにキャストできるか？
    pOther = new Other();
    pBase  = dynamic_cast<Base*>(pOther);
    cout << (pBase ? "cast ok" : "cast fail") << endl;
    delete pBase;
}

cast ok
cast fail
cast fail
cast fail


7章：STL

1. STLとは

    STL(Standard Template Library)とは、データを格納するコンポーネントと、そのコンポーネントを 操作するためのアルゴリズム群であり、C++の標準の機能である。

    コンポーネントには、「ベクター」、「リスト」、「キュー」、「スタック」をはじめさまざまな コンポーネントが標準で用意されている。それぞれ一長一短で、用途によって使い分けることができる。 また自分で作成することもできる。

    また、アルゴリズムには「データの追加」、「ソート」、「検索」など有用なアルゴリズムが 用意されている。

    これらは、int型、char型、ポインタ型など言語自体で定義されている型以外にも、自分で定義した 構造体やクラスなどを扱うことも可能である。

    このようなコンポーネントとアルゴリズムを組みあわせることにより、自分でデータ格納や データ整理などに関する関数やクラスを作らなくてもよいため、プログラムの部品化が可能で、 再利用性の高いプログラムを作ることができる。

    MFCでは、リストやベクターなどのクラスが定義されているが、MFCは当然Windows上でのみ 使用できる。これに対して、STLでは、C++の機能であるため、他のOSでも使用することができる。


7章：STL

2. STLの歴史

    STLは1979年Alexander Stepanov氏によって考案され、1994年にANSIに含めることを決定しました。 この間、HP社が開発に加わったため、STLにはHP社のバージョンとANSI規格のSTLと２つの種類がありますが、 ほとんど違いはありません。


7章：STL

3. STLの構成

    STLにはデータを格納する「コンテナ」と、データを操作する「アルゴリズム」からなることは すでに説明しました。実は「反復子(イタレーター)」を含めた３つのものから構成されています。

    イタレーターとは、ポインタのようなもので、コンテナに格納されているデータの場所を示すものです。 コンテナとアルゴリズムとを直接関係づけるのではなく、コンテナとアルゴリズムの間に、イタレーターを はさむことによって、コンテナはアルゴリズムを、アルゴリズムはコンテナを意識せずに使うこと(作ること)が できるのです。


7章：STL

4. コンテナ

    早速、実装と行きましょう。でもその前に、データの格納するコンテナは、一長一短だと 説明しました。つまり、どんなふうにデータを格納するか、コンテナをどんなふうに利用するか によって最適なコンテナを選ばなければいけません。

    そこで、まず標準で用意されているコンテナを紹介します。

コンテナ 特　　徴 ヘッダファイル
vector 動的な配列

実行時に配列の大きさを変えることができる配列です。
普通の配列のようにランダムにデータにアクセスすることが多い場合に有効です。
データ数(要素数)が変化する場合(追加したり、削除したりする場合)には、その度に新しいメモリーが 割り当てられるため効率が悪くなります。すなわち、要素数の変化が多い場合は不向きです。

vector
list 双方向リスト

双方向にアクセス可能な線形リストです
最初から最後へ、最後から最初へ順番(シーケンシャル)に参照していく場合に有効です。
ランダムアクセスには不向きです。
データの追加、削除を頻繁に行うデータ構造に有効です。

list
queue キュー

先入れ先出し(FIFO)のキューです
コンテナの終端にデータを追加し、コンテナの先端からデータを取得するのに適したコンテナです

queue
deque 両端キュー

コンテナの両端からアクセスできるキューです
コンテナの両端へのデータ追加、両端からのデータ取得に適したキューです

deque
priority_queue 優先順位付きキュー

優先順位を管理する機能のついたキューです

queue
stack スタック

先入れ後出し(FILO)のスタックです
コンテナの終端にデータを追加し、コンテナの終端からデータを取得するのに適したコンテナです

stack
map マップ

表を作るのに適したコンテナです
キーと値という２つのペアを対応表として格納するのに適したコンテナです
キーの重複は許されません。

map
mutimap マルチマップ

表を作るのに適したコンテナです
mapはキーが重複してはいけないのに対し、同じキーで複数のデータを登録できる表です

map
set セット

ソートしながら要素を追加できるコンテナです。
データの重複は許されません。

set
multiset マルチセット

データの重複が許されるセットです

set
bitset ビットセット

true / falseを格納するためのコンテナです

set
    各コンテナは、代入演算子や比較演算子などが定義されていますので、 同じコンテナを複数コピーしたり、比較したりすることもできます。この場合、 要素すべてがコピーされたり、比較したりしますので、時間を要する処理になります。

    また、コンテナにクラスのポインタを格納する場合には注意が必要です。 例えば、コンテナをコピーした場合、ポインタがコピーされますので、１つの コンテナ内に格納されているポインタを削除(delete)すれば、もう１つのコンテナ内に 格納されている同じポインタを使って何かすることができなくなるからです。


7章：STL

5. vector

    では、最初に一番簡単なvectorコンテナについて説明します。

    vectorはすでに説明したように、動的な配列ですので、ランダムアクセスに適していますが、 要素の追加、削除を頻繁に行うような場合には適しません。

パフォーマンス
作業内容 | 処理速度
要素の追加 | 線形時間(要素数に比例して時間がかかる)
要素の削除 | 線形時間(要素数に比例して時間がかかる)
ランダムアクセス | 定数時間(要素数によらない)

vectorクラスの主要な関数
関数名 | 書　式 | 説　明
begin | iterator begin() | 先頭を表わすイタレーターを返します
end iterator end() 末尾を表わすイタレーターを返します
末尾とは最後の要素の次を表わします
rbegin reverse_iterator rbegin() 末尾を表わす逆方向イタレーターを返します
末尾とは最後の要素の次を表わします
rend | reverse_iterator rend() | 先頭を表わす逆方向イタレーターを返します
empty | bool empty() | vectorの要素が0なら「true」を、カラでなければ「false」を返します
size | int size() | 要素数を返します
push_back | void push_back( const T &value ) | vectorの末尾にデータを追加します
pop_back | void pop_back() | vectorの末尾の要素を削除します
insert | iterator insert( iterator itr, const T &value ) | itrの直前にvalueを挿入します
insert | iterator insert( iterator itr, size_t num, const T &value ) | itrの直前にvalueをnum個挿入します
insert | iterator insert( iterator itr, const_iterator first, const_iterator last ) | itrの直前にfirstからlastの間のデータをコピーして挿入します
erase | iterator erase( iterator itr ) | itrの示す要素を削除します
erase | iterator erase( iterator start, iterator end ) | startからendまでの要素を削除します
clear | void clear() | すべての要素を削除します
front | reference front() | vectorの先端の要素を返します
back | reference back() | vectorの末尾の要素を返します
at | reference at( size_type pos ) | pos番目の要素を返します
operator[] reference operator[]( size_type pos ) pos番目の要素を返します
(できるだけ「at」を使用することをお薦めします)
assign | void assign( const iterator first, const iterator last ) | vector(自分自身)の内容をfirstからlastに置き換えます
assign | void assign( size_type n, const T& x = T() ) | vector(自分自身)の内容をn個のxに置き換えます
swap | void swap( vector x ) | vector(自分自身)の内容をxと交換します

コンストラクタ
書　式 説　明
vector(const A& al = A() )
例) vector<int> vect;
長さ0のint型のvectorを作る
vector(size_type n, const T& v = T(), const A& al = A() )
例) vector<int> vect2(20)
長さ20のint型vectorを作る
vector(const vector& x )
例) vector<int> vect3(10, 100);
長さ10のint型vectorを作り、すべての要素を100で初期化する
vector(const_iterator first, const_iterator last, const A& al = A() )
例) vector<int> vect4(vect1);
vect1をコピーし、vect4とする
    いきなり、たくさんの関数が出てきたため、やる気がなくなったかもしれませんが、整理して覚えると そんなに難しくはありません。

    vectorは要素の追加や削除が行われた場合、新しく領域が確保されるため、それまでに使用していた イタレーターや参照は使用できなくなります。

    次のページでは実際に使ってみます。


7章：STL

    では、早速サンプルを紹介します。
#include <iostream>

// vectorを使えるようにする
#include<vector>
using namespace std;

int main(int argc, char* argv[]){
    int  i;

    // 空のvectorを作る
    vector<int>  vect;

    // vectorに数字を代入する
    for( i = 0 ; i < 10 ; i++ )
        vect.push_back( 2*i );

    // vectorの中身を表示する
    cout << "vector" << endl;
    for( i = 0 ; i < vect.size() ; i++ )
        cout << i << "   " << vect.at(i) << endl;

    return 0;
}

vector
0   0
1   2
2   4
3   6
4   8
5   10
6   12
7   14
8   16
9   18
    上の例のように、使い方はint型のvectorを定義し、push_back関数で追加し、at関数で取得するだけです。

ここまではイタレーターも出現していないし、そんなに難しくはありません。


7章：STL

    次のサンプルは、データを追加した後、削除していくサンプルです。このサンプルのように データの追加、削除を行う用途にはvectorは不向きですが、サンプルですので、深く考えないで ください。
#include <iostream>

// vectorを使えるようにする
#include<vector>
using namespace std;

// 長さ10のchar型vectorを作る
vector<char>   vect(10);

//
// vectorの中身を表示する
void disp(){
    cout << "vector   ";
    for( int i = 0 ; i < vect.size() ; i++ )
        cout << vect.at( i );
    cout << endl;
}

int main(int argc, char* argv[]){
    int	i;

    // vectorに文字を代入する
    // 今回は、あらかじめ10個分のvectorを用意し、それに代入する
    // あらかじめ代入するデータの個数がわかっている場合は、先ほどの例より、
    // こちらの方が効率が良い。なぜならば、データ追加(push_back)の度に、
    // vectorの領域を確保しなおす必要がなくなるから。
    for( i = 0 ; i < 10 ; i++ )
        vect.at( i ) = 'A'+i;

    // vectorの中身を表示
    disp();

    // vectorが空でなければ、削除して内容を表示する
    while( !vect.empty() ){
        vect.pop_back();
        disp();
    }

    return 0;
}

vector   ABCDEFGHIJ
vector   ABCDEFGHI
vector   ABCDEFGH
vector   ABCDEFG
vector   ABCDEF
vector   ABCDE
vector   ABCD
vector   ABC
vector   AB
vector   A
vector
    前回のサンプルと今回のサンプルの異なる点は、vectorを定義する場合に、あらかじめ10個の 領域を確保しているところです。前回のサンプルでは「push_back」を10回実行しているため、 vectorクラスの中では、プログラマーの知らないあいだに、push_backを実行する度に、新しく メモリーを割り当てて、新しいメモリーに古いデータ群をコピーし、新しく追加するデータを コピーするという作業が行われています。したがって、10回push_backを行うということは、 とても効率が悪いのです。

    それに対して、今回のようにあらかじめ10個分の領域を確保しておけば、代入するだけですので 新しくメモリー領域を確保し、コピーをするという作業がなくなり効率が良くなります。

    今回のサンプルでは、vectorの末尾から１つずつデータを消去しています。これをvectorが 空になるまで(empty()関数を使用)繰り返しています。

    このpop_back関数も、vectorの領域の大きさが変更されますので、新たな領域を確保し、 古いデータ群をコピーするという作業が行われ、効率が悪くなります。一度に複数のデータを 削除する場合は、「erase」関数を使う方が効率が良くなります。また、すべてを削除するので あれば、clear関数を使用します。

    erase関数の引数にイタレーターを使用しますので、次にすべてを削除する例を紹介します。

#include <iostream>

// vectorを使えるようにする
#include<vector>
using namespace std;

// 長さ10のchar型vectorを作る
vector<char>   vect(10);

//
// vectorの中身を表示する
void disp(){
    cout << "vector   ";
    for( int i = 0 ; i < vect.size() ; i++ )
        cout << vect.at( i );
    cout << endl;
}

int main(int argc, char* argv[]){
    int	i;

    // vectorに文字を代入する
    for( i = 0 ; i < 10 ; i++ )
        vect.at( i ) = 'A'+i;

    // vectorの中身を表示
    disp();

    // vectorが空でなければ、すべてを削除して内容を表示する
    while( !vect.empty() ){
        vect.clear();
        disp();
    }

    return 0;
}

vector   ABCDEFGHIJ
vector


7章：STL

    次のサンプルは、swap関数、assign関数、代入演算子(=)の使用方法です。
#include <iostream>

// vectorを使えるようにする
#include<vector>
using namespace std;

// 空のint型vectorを作る
vector<int>  vect1;
vector<int>  vect2;

//
// vectorの中身を表示する
void disp(){
    int i;

    cout << "vect1  : ";
    for( i = 0 ; i < vect1.size() ; i++ )
        cout << vect1.at( i ) << ' ';
    cout << endl;

    cout << "vect2  : ";
    for( i = 0 ; i < vect2.size() ; i++ )
        cout << vect2.at( i ) << ' ';
    cout < endl < endl;
}

void main(){
    int  i;

    // vectorに数字を代入する
    for( i = 0 ; i < 10 ; i++ ){
        vect1.push_back( 2*i );
        vect2.push_back( 2*i+1 );
    }

    // vectorの中身を表示する
    disp();

    cout << "vect1とvect2 を交換(swap)" << endl;
    vect1.swap( vect2 );
    disp();

    cout << "vect1にvect2をコピー(assign)" << endl;
    vect1.assign( vect2.begin(), vect2.end() );
    disp();

    cout << "vect1を10個の20に置き換える" << endl;
    vect1.assign( 10, 20 );
    disp();

    cout << "コピーは代入演算子(=)でもOK" << endl;
    cout << "vect2にvect1をコピー(=)" << endl;
    vect2 = vect1 ;
    disp();
}

vect1  : 0 2 4 6 8 10 12 14 16 18
vect2  : 1 3 5 7 9 11 13 15 17 19

vect1とvect2 を交換(swap)
vect1  : 1 3 5 7 9 11 13 15 17 19
vect2  : 0 2 4 6 8 10 12 14 16 18

vect1にvect2をコピー(assign)
vect1  : 0 2 4 6 8 10 12 14 16 18
vect2  : 0 2 4 6 8 10 12 14 16 18

vect1を10個の20に置き換える
vect1  : 20 20 20 20 20 20 20 20 20 20
vect2  : 0 2 4 6 8 10 12 14 16 18

コピーは代入演算子(=)でもOK
vect2にvect1をコピー(=)
vect1  : 20 20 20 20 20 20 20 20 20 20
vect2  : 20 20 20 20 20 20 20 20 20 20
    今回は、代入演算子(=)、置換(assign)関数そして、交換(swap)関数の使い方を紹介しました。 これらは今後他のコンテナにも出てきますので、覚えて置いてください。


7章：STL

6. イタレーター

    vectorは、C言語の配列を拡張したものであるため、イタレーターを使用せずとも[]演算子や、 at関数を使用すれば、配列として使用できることはおわかりいただけたと思います。

    しかし数多く用意されているアルゴリズムを使用するためにはイタレーターが必要になってきます。 そこでイタレーターについて少し説明します。

    イタレーターはポインタのようなものだと説明しました。ポインタは
char*   p = "abcdefg";
p++;		// (1)ポインタを１つ進める
p--;		// (2)ポインタを１つ戻す
p+=5;		// (3)ポインタを5つ進める
p-=5;		// (4)ポインタを5つ戻す
*p = 'A';		// (5)ポインタの指す内容を書き換える
char ch = *p	// (6) ポインタの指す内容を取得する
のようにポインタを進めたり、戻したり、内容を参照したりできます。

    イタレーターでは、これらすべてが可能なイタレーターとそうではないものに分けられ、どんな機能を 有するイタレーターなのかで、５種類に分類できます。

名　称 機　能
入力イタレーター (1)ポインタを一つ進める
(6)ポインタの指す内容を取得する
出力イタレーター (1)ポインタを一つ進める
(5)ポインタの指す内容を書き換える
前方イタレーター (1)ポインタを一つ進める
(5)ポインタの指す内容を書き換える
(6)ポインタの指す内容を取得する
双方向イタレーター (1)ポインタを一つ進める
(2)ポインタを１つ戻す
(5)ポインタの指す内容を書き換える
(6)ポインタの指す内容を取得する
ランダムアクセスイタレーター (1)ポインタを一つ進める
(2)ポインタを１つ戻す
(3)ポインタをn個進める
(4)ポインタをn個戻す
(5)ポインタの指す内容を書き換える
(6)ポインタの指す内容を取得する
    そして、使用するコンテナによってはどのイタレーターを使用できるかに制限があります。 例えば、すでに紹介したvectorでは、ランダムアクセスイタレーターを 使用できますが、これから紹介するlistでは、双方向イタレーターしか 使用することができません。

    さらに、アルゴリムを使用するためにも、制限があります。ソートを行うsort()というアルゴリズムを 使用するためにはランダムアクセスイタレーターが必要です。したがって、listコンテナでは アルゴリズムのsort()を使用することができません。(ただし、listクラスのメンバー関数として sort()が定義されているので、そちらを使用すれば、ソートが可能)


7章：STL

    まず簡単な例として、vector内の要素をイタレーターを利用してすべて表示させるプログラムを紹介する。
#include <iostream>

// vectorを使えるようにする
#include<vector>
using namespace std;

// 長さ10のchar型vectorを作る
vector<char>   vect(10);

//
// vectorの中身を表示する(イタレーター使用)
void disp1(){
    vector<char>::iterator   itr    = vect.begin();
    vector<char>::iterator   itrEnd = vect.end();

    cout << "vector   ";

    for( ; itr != itrEnd ; itr++ )
        cout << *itr << " ">;
    cout << endl;
}
//
// vectorの中身を表示する(at()関数使用)
void disp2(){
    cout << "vector   ";

    for( int i = 0 ; i < vect.size() ; i++ )
        cout << vect.at(i) << " ";
    cout << endl;
}

int main(int argc, char* argv[]){
    int	i;

    // vectorに文字を代入する
    for( i = 0 ; i < 10 ; i++ )
        vect.at( i ) = 'A'+i;

    // vectorの中身を表示
    disp1();
    disp2();

    return 0;
}

vector   A B C D E F G H I J
vector   A B C D E F G H I J
    上記の例のように、ほとんどポインタと同じように使用することができる。つまり、 「itr++」や「itr--」で、イタレーターを１つ進めたり、戻したりできる。また、 「*itr」で、イタレーターが指すデータそのものを表わすことができる。

    さらに「itr += 3」で３つ進めたり、「itr -= 3」で３つ戻したり、 「itr2 - itr1」のように、差を求めることもできる。

    このようなポインタとほぼ同じことができるイタレーターを前ページで 紹介した「ランダムアクセスイタレーター」と言う。

    listコンテナで採用されている「双方向イタレーター」では、差を求めたり、複数個進めたり戻したり といったことはできない。つまり「itr += 3」、「itr -= 3」、「itr2 - itr1」 のような使い方はできない

    さらに「前方イタレーター」では、イタレーターを戻すことができないので「itr--」が 使用できない。そして「出力イタレーター」では内容取得ができないため、「n = *itr」が できないし、「入力イタレーター」では内容の書き換えができないため、「*itr = n」が 使用できない。


7章：STL

    次に、アルゴリズムを使った例を紹介する。アルゴリズムの詳細については後述するので、ここでは アルゴリズムのソートとシャッフルを用いた。
#include <iostream>

// vectorを使えるようにする
#include<vector>
using namespace std;

// 長さ10のchar型vectorを作る
vector<char>   vect(10);

//
// vectorの中身を表示する(イタレーター使用)
void disp(){
    vector<char>::iterator   itr    = vect.begin();
    vector<char>::iterator   itrEnd = vect.end();

    cout << "vector   ";

    for( ; itr != itrEnd ; itr++ )
        cout << *itr << " ">;
    cout << endl;
}

int main(int argc, char* argv[]){
    int	i;

    // vectorに文字を代入する
    for( i = 0 ; i < 10 ; i++ )
        vect.at( i ) = 'A'+i;

    // vectorの中身を表示
    disp();

    // シャッフル
    random_shuffle( vect.begin(), vect.end() );
    disp();

    // ソート
    sort( vect.begin(), vect.end() );
    disp();

    return 0;
}

vector   A B C D E F G H I J
vector   E D A C G H I J F B
vector   A B C D E F G H I J
    ソートおよび、シャッフルはその範囲を表わすイタレーターを引数にとる。したがって、 vector５つ分をシャッフルするならば、
    // vectorの中身を表示
    disp();

    // シャッフル
    random_shuffle( vect.begin(), vect.begin()+5 );
    disp();

    // ソート
    sort( vect.begin(), vect.begin()+5 );
    disp();

vector   A B C D E F G H I J
vector   E D A C B F G H I J
vector   A B C D E F G H I J
のようにすればよい。


7章：STL

逆方向イタレーター(逆方向反復子)

    逆方向イタレーターとは、今まで紹介してきたイタレーターの逆順を示すものである。 今まで出てきたイタレーターは、「begin()」関数で取得したイタレーターに 対して「itr++」のようにして次のデータにアクセスしてきた。

    逆方向イタレーターは「rItr++」のようにすると、１つ前のデータを 指すようになる。またアルゴリズムも逆順になるため、ソート関数の引数に逆方向イタレーターを 渡すと、降順ソートになる。

    以下に、逆方向イタレーターの例を示す。これには逆方向に表示する例と、降順ソートの 例が含まれている。

#include <iostream>

// vectorを使えるようにする
#include<vector>
#include<algorithm>
using namespace std;

// 空のint型vectorを作る
vector<int>  vect;

//
// vectorの中身を表示する
void disp(){
    vector<int>::iterator itr    = vect.begin();
    vector<int>::iterator itrEnd = vect.end();

    cout << "vect  : ";
    for( ; itr != itrEnd ; itr++ )
        cout << *itr << ' ';
    cout << endl;
}

//
// vectorの中身を逆順に表示する
void rdisp(){
    vector<int>::reverse_iterator rItr    = vect.rbegin();
    vector<int>::reverse_iterator rItrEnd = vect.rend();

    cout << "vect  : ";
    for( ; rItr != rItrEnd ; rItr++ )
        cout << *rItr << " ";
    cout << endl;
}

void main(){
    int  i;

    // vectorに数字を代入する
    for( i = 0 ; i < 10 ; i++ )
        vect.push_back( 2*i );

    // vectorの中身を表示する
    cout << "vectorを表示します" << endl;
    disp();
    cout << endl;

    // vectorの中身を逆順表示する
    cout << "vectorを逆順で表示します" << endl;
    rdisp();
    cout << endl;

    // vectorの中身を降順ソートする
    cout << "vectorを降順ソートし、表示します" << endl;
    sort( vect.rbegin(), vect.rend() );
    disp();
    cout << endl;

    // vectorの中身を昇順ソートする
    cout << "vectorを昇順ソートし、表示します" << endl;
    sort( vect.begin(), vect.end() );
    disp();
    cout << endl;
}

vectorを表示します
vect  : 0 2 4 6 8 10 12 14 16 18

vectorを逆順で表示します
vect  : 18 16 14 12 10 8 6 4 2 0

vectorを降順ソートし、表示します
vect  : 18 16 14 12 10 8 6 4 2 0

vectorを昇順ソートし、表示します
vect  : 0 2 4 6 8 10 12 14 16 18


7章：STL

7. クラスとSTL

    当然ながら自分で作成した構造体やクラスをvectorなどのコンテナに追加することもできます。 しかし、アルゴリズムを使用するためには、必要なオペレーターを実装する必要がありまあす。

    例えば、シャッフルやソートでは、データを入れ換える操作を行う必要があります。 データの入れ換えを行うということは、コピーコンストラクタが使用されます。つまり コピーコンストラクタを定義しなければなりません。ただし、単純にメンバー変数のコピー だけを行うのであれば、プログラマーが定義しなくてもコンパイラーが自動で定義してくれるため 不要です。

    さらにソートを行うためには、各要素の比較をしなければなりません。つまり、 比較演算子(operator <)を定義しなければなりません。

    また、検索を行うためには、等価かどうかを調べる必要があります。つまり、 比較演算子(operator ==)を定義しなければなりません。

#include <iostream>

// vectorを使えるようにする
#include<vector>
using namespace std;

class TestClass{
public:
    // コンストラクタ
    TestClass(int n, char ch){
        m_n  = n;
        m_ch = ch;
    }

    // コピーコンストラクタは、メンバー変数のコピーなので必要ないが一応作っておく
    // [const]を忘れると、エラーになる
    TestClass(const TestClass &source){
        m_n  = source.m_n;
        m_ch = source.m_ch;
    }

    // 比較演算子(findで必要)
    // [const]を忘れるとエラー
    operator==(const TestClass &a)const{
        return m_n == a.m_n && m_ch == a.m_ch ;
    }

    // 比較演算子(sortで必要)
    // [const]を忘れるとエラー
    operator<(const TestClass &a)const{
        return m_n < a.m_n;
    }

    int  m_n;
    char m_ch;
};

// TestClass型のvectorを作る
vector<TestClass>   vect;

//
// vectorの中身を表示する(イタレーター使用)
void disp(){
    vector<TestClass>::iterator	itr    = vect.begin();
    vector<TestClass>::iterator	itrEnd = vect.end();

    cout << "vector   " << endl;

    for( ; itr != itrEnd ; itr++ )
        // *演算子よりも、.演算子の方が優先順位が高いので、カッコでくくる必要がある
        cout << "n = " << (*itr).m_n << "  ch = " << (*itr).m_ch << endl;
    cout << endl;
}

void main(){
    int    i;

    // vectorに代入する
    for( i = 0 ; i < 10 ; i++ )
        vect.push_back( TestClass(i, 'A'+i) );

    // vectorの中身を表示
    disp();

    // シャッフル
    random_shuffle( vect.begin(), vect.end() );
    disp();

    // ソート
    sort( vect.begin(), vect.end() );
    disp();

    // 検索
    TestClass   test(2, 'C');                               // 比較対象
    vector<TestClass>::iterator itr;                        // 検索結果を入れるイタレーター
    vector<TestClass>::iterator itrBegin = vect.begin();    // 最初を示すイタレーター
    vector<TestClass>::iterator itrEnd   = vect.end();      // 末尾を示すイタレーター
    if( (itr = find( itrBegin, itrEnd, test )) != itrEnd )  // 検索
        cout << "testは" << itr - itrBegin << "番めに見つかりました" << endl;
    else
        cout << "testは見つかりませんでした" << endl;

    return 0;
}

vector
n = 0  ch = A
n = 1  ch = B
n = 2  ch = C
n = 3  ch = D
n = 4  ch = E
n = 5  ch = F
n = 6  ch = G
n = 7  ch = H
n = 8  ch = I
n = 9  ch = J

vector
n = 4  ch = E
n = 3  ch = D
n = 0  ch = A
n = 2  ch = C
n = 6  ch = G
n = 7  ch = H
n = 8  ch = I
n = 9  ch = J
n = 5  ch = F
n = 1  ch = B

vector
n = 0  ch = A
n = 1  ch = B
n = 2  ch = C
n = 3  ch = D
n = 4  ch = E
n = 5  ch = F
n = 6  ch = G
n = 7  ch = H
n = 8  ch = I
n = 9  ch = J

testは2に見つかりました


7章：STL

    クラスのポインタ型のコンテナを作り、newしたインスタンスをコンテナに追加していくような 使い方をする場合は注意が必要です。例えば、次のような場合、vectorに代入されるのは ポインタです。したがってソートなどのアルゴリズムから呼ばれる演算子は、クラスの比較演算子ではなく、 ポインタ同士の比較演算子になります。つまり、ポインタ(アドレス)でのソートとなり、あまり 有用な使い方がなくなってしまいます。(クラスのポインタ演算子を作ることができれば、 このような事はなくなるのですが・・・。)
// この場合、ソート関数から呼ばれるのは、この演算子ではない
bool operator<(TestClass& p1, const TestClass& p2){
    return p1 < p2;
}

// このような演算子は作れない
bool operator<(TestClass*& p1, const TestClass*& p2){
    return *p1 < *p2;
}

{
    // TestClass型のvectorを作る
    vector<TestClass*>   vect;

    // vectorに代入する
    for( i = 0 ; i < 10 ; i++ )
        vect.push_back( new TestClass(i, 'A'+i) );

    sort( vect.begin(), vect.end() );
    // ソート関数内から呼ばれる比較演算子は、ポインタ同士の比較をする演算子である
    // つまり「bool operator<(TestClass*& p1, const TestClass*& p2)」という演算子であるが、
    // このような演算子はプログラマーが勝手に用意することはできない
}
    つまり、以下のサンプルではソートを行っているが、アドレス順にソートされているため、見かけ上 ソートされているようにはみえない。
#include "stdafx.h"
#include <algorithm>
#include <iostream>

// vectorを使えるようにする
#include<vector>
using namespace std;

class TestClass{
public:
    // コンストラクタ
    TestClass(int n, char ch){
        m_n  = n;
        m_ch = ch;
    }

    // 比較演算子(sortで必要)
    bool operator<(const TestClass &a)const{
        return m_n < a.m_n;
    }

    int  m_n;
    char m_ch;
};

// TestClass型のvectorを作る
vector<TestClass*>   vect(10);

//
// vectorの中身を表示する(イタレーター使用)
void disp(){
    vector<TestCclass*>::iterator	itr = vect.begin();

    cout << "vector   " << endl;
    for( ; itr != vect.end() ; itr++ )
        cout << "n = " << (**itr).m_n << "  ch = " << (**itr).m_ch << "  address:" << *itr << endl;
    cout << endl;
}

void main(){
    // vectorに追加する
    for( int i = 0 ; i < 10 ; i++ )
        vect[i] = new TestClass(i, 'A'+i);

    random_shuffle( vect.begin(), vect.end() );
    disp();

    // ソート
    sort( vect.begin(), vect.end() );
    disp();

    return;
}

vector
n = 4  ch = E  address:007E0510
n = 3  ch = D  address:007E0E70
n = 0  ch = A  address:007E0C30
n = 2  ch = C  address:007E0EB0
n = 6  ch = G  address:007E0490
n = 7  ch = H  address:007E0450
n = 8  ch = I  address:007E0410
n = 9  ch = J  address:007E03D0
n = 5  ch = F  address:007E04D0
n = 1  ch = B  address:007E0BF0

vector
n = 9  ch = J  address:007E03D0
n = 8  ch = I  address:007E0410
n = 7  ch = H  address:007E0450
n = 6  ch = G  address:007E0490
n = 5  ch = F  address:007E04D0
n = 4  ch = E  address:007E0510
n = 1  ch = B  address:007E0BF0
n = 0  ch = A  address:007E0C30
n = 3  ch = D  address:007E0E70
n = 2  ch = C  address:007E0EB0
    このような場合は、比較するための関数を自分で作成し(下の例では「small」関数)、 sort関数などから比較のために、作成した関数を呼ぶように定義する。これを関数オブジェクトと呼ぶ。

    以下の例では関数オブジェクトを利用しているため、クラスポインタのコンテナではあるが、 きちんとソートされている例である。

#include "stdafx.h"
#include <algorithm>
#include <iostream>

// vectorを使えるようにする
#include<vector>
using namespace std;

class TestClass{
public:
    // コンストラクタ
    TestClass(int n, char ch){
        m_n  = n;
        m_ch = ch;
    }

    // 比較演算子(findで必要)
    bool operator==(const TestClass &a)const{
        return m_n == a.m_n && m_ch == a.m_ch ;
    }

    // 比較演算子(sortで必要)
    bool operator<(const TestClass &a)const{
        return m_n < a.m_n;
    }

    int  m_n;
    char m_ch;
};

bool small(const TestClass* &a, const TestClass* &b){
    return *a < *b;
}

// TestClass型のvectorを作る
vector<TestClass*>   vect(10);

//
// vectorの中身を表示する(イタレーター使用)
void disp(){
    vector<TestClass*>::iterator	itr = vect.begin();

    cout << "vector   " << endl;
    for( ; itr != vect.end() ; itr++ )
        cout << "n = " << (**itr).m_n << "  ch = " << (**itr).m_ch << "  address:" << *itr << endl;
    cout << endl;
}

void main(){
    // vectorに追加する
    for( int i = 0 ; i < 10 ; i++ )
        vect[i] = new TestClass(i, 'A'+i);

    random_shuffle( vect.begin(), vect.end() );
    disp();

    // ソート
    sort( vect.begin(), vect.end(), ::small  );
    disp();

    return;
}

vector
n = 4  ch = E  address:007E0510
n = 3  ch = D  address:007E0E70
n = 0  ch = A  address:007E0C30
n = 2  ch = C  address:007E0EB0
n = 6  ch = G  address:007E0490
n = 7  ch = H  address:007E0450
n = 8  ch = I  address:007E0410
n = 9  ch = J  address:007E03D0
n = 5  ch = F  address:007E04D0
n = 1  ch = B  address:007E0BF0

vector
n = 0  ch = A  address:007E0C30
n = 1  ch = B  address:007E0BF0
n = 2  ch = C  address:007E0EB0
n = 3  ch = D  address:007E0E70
n = 4  ch = E  address:007E0510
n = 5  ch = F  address:007E04D0
n = 6  ch = G  address:007E0490
n = 7  ch = H  address:007E0450
n = 8  ch = I  address:007E0410
n = 9  ch = J  address:007E03D0
    このように関数オブジェクトを使用して、比較などをすでに定義されている演算子などを利用しない 方法を用いることで、クラスポインタ型のコンテナを利用する。

    ソート関数は関数オブジェクトを使用できるが、検索を行う「find」関数は、関数 オブジェクトを使用できない。この場合は「find_if」関数を用いればよい。このように アルゴリズムに定義されている関数には、関数オブジェクトを利用できる関数と、「○○_if」という 関数オブジェクトを使用するための専用の関数が用意されているのでそちらを使う。


7章：STL

8. list

    listはすでに説明したように、双方向にアクセス可能な線形リストです。最初から最後へ、最後から 最初へ順番(シーケンシャル)に参照していく場合に有効ですが、ランダムアクセスには不向きです。 データの追加、削除を頻繁に行うデータ構造に有効です。

パフォーマンス
作業内容 | 処理速度
要素の追加 | 定数時間(要素数によらない)
要素の削除 | 定数時間(要素数によらない)
ランダムアクセス | 不可能(強いて言うなら線形時間(要素数に比例して時間がかかる))

listクラスの主要な関数
関数名 | 書　式 | 説　明
begin | iterator begin() | 先頭を表わすイタレーターを返します
end iterator end() 末尾を表わすイタレーターを返します
末尾とは最後の要素の次を表わします
rbegin reverse_iterator rbegin() 末尾を表わす逆方向イタレーターを返します
末尾とは最後の要素の次を表わします
rend | reverse_iterator rend() | 先頭を表わす逆方向イタレーターを返します
empty | bool empty() | listの要素が0なら「true」を、カラでなければ「false」を返します
size | int size() | 要素数を返します
push_back | void push_back( const T &value ) | listの末尾にデータを追加します
push_front | void push_front( const T &value ) | listの先頭にデータを追加します
pop_back | void pop_back() | listの末尾の要素を削除します
pop_front | void pop_front() | listの先頭の要素を削除します
insert | iterator insert( iterator itr, const T &value ) | itrの直前にvalueを挿入します
insert | iterator insert( iterator itr, size_t num, const T &value ) | itrの直前にvalueをnum個挿入します
insert | iterator insert( iterator itr, const_iterator first, const_iterator last ) | itrの直前にfirstからlastの間のデータをコピーして挿入します
erase | iterator erase( iterator itr ) | itrの示す要素を削除します
erase | iterator erase( iterator start, iterator end ) | startからendまでの要素を削除します
clear | void clear() | すべての要素を削除します
front | reference front() | listの先端の要素を返します
back | reference back() | listの末尾の要素を返します
remove | void remove(const T& value) | valueと同じ要素を削除します
reverse | void reverse( ) | リストの順番を逆順にします
sort | void sort( ) | リストを昇順にソートします
merge void merge( list& lst ) 引数lstの要素を現在のリストに追加し、lstの要素をすべて削除します
２つのリストが昇順にソートされていれば、できたリストも昇順にソートされている
unique | void unique( ) | リスト中に同じ要素が連続していた場合、１つを残して削除します
assign | void assign( const iterator first, const iterator last ) | list(自分自身)の内容をfirstからlastに置き換えます
assign | void assign( size_type n, const T& x = T() ) | list(自分自身)の内容をn個のxに置き換えます
swap | void swap( list x ) | list(自分自身)の内容をxと交換します

コンストラクタ
書　式 説　明
list(const A& al = A())
例) list<int> list1;
長さ0のint型のlist1を作る
list(size_type n, const T& v = T(), const A& al = A())
例) list<int> list2(20)
長さ20のint型list2を作る
list(const list& x)
例) list<int> list3(10, 100);
長さ10のint型list3を作り、すべての要素を100で初期化する
list(const_iterator first, const_iterator last, const A& al = A())
例) list<int> list4(list1);
list1をコピーし、list4とする
    listで使用されるイタレーターは双方向イタレーターであるため、vectorのようなat関数や[]オペレーター は使用できません。その代わり、push_frontのような関数が追加されています。また、ソートを行う場合は、 ランダムアクセスイタレーターが使用できないため、アルゴリズムのsort関数ではなく、listクラスで用意 されているsort関数を使用します。

    vectorでは、要素の追加や削除が行われた場合、すべての領域が新しく確保されていたため、それまで 使用していたイタレーターや参照は使用できなくなりました。これに対してlistでは、リストの再割り当てが 行われるのはごく一部だけなので、削除された部分を示すイタレーターや参照のみが使用できなくなり、 それ以外のイタレーターや参照はそのまま使用できます。

    次のページでは実際に使ってみます。


7章：STL

    ２つのリストをマージする例を示します。
#include <iostream>

// listを使えるようにする
#include<list>
using namespace std;

//
// リストの表示
void disp( list<int>& lst ){
    // イタレーターの宣言
    list<int>::iterator itr    = lst.begin();
    list<int>::iterator itrEnd = lst.end();

    for( ; itr != lst.end() ; itr++ )
        cout << *itr << " ";
    cout << endl;
}

int main(int argc, char* argv[]){
    // リストを宣言
    list<int>  lst1, lst2, lst3, lst4;
    int        i, n;

    // リストに追加
    for( i = 0 ; i < 5 ; i++ ){
        n = rand() % 10;
        // listなので、push_frontが使える
        lst1.push_front( n );
        lst3.push_front( n );
    }

    for( i = 0 ; i < 5 ; i++ ){
        n = rand() % 10;
        lst2.push_front( n );
        lst4.push_front( n );
    }

    // lst1の表示
    cout << "list1  ";
    disp( lst1 );
    cout << endl;

    // lst2の表示
    cout << "list2  ";
    disp( lst2 );
    cout << endl;

    // リストをマージ
    lst1.merge( lst2 );

    // lst1の表示
    cout << "list1  ";
    disp( lst1 );
    cout << "↑ソートされていないリストをマージしたので、結果の順番もめちゃくちゃ" << endl << endl;

    // ソートしてからマージしてみる
    lst3.sort();
    lst4.sort();
    lst3.merge( lst4 );

    // lst3の表示
    cout << "list3  ";
    disp( lst3 );
    cout << "↑ソートされたリストをマージしたので、結果の順番もソートされている" << endl << endl;

    // lst2の表示
    cout << "list2  ";
    disp( lst2 );
    cout << "list4  ";
    disp( lst4 );
    cout << "↑追加した方はからになっている" << endl;

    return 0;
}

list1  9 0 4 7 1

list2  4 2 8 8 4

list1  4 2 8 8 4 9 0 4 7 1
↑ソートされていないリストをマージしたので、結果の順番もめちゃくちゃ

list3  0 1 2 4 4 4 7 8 8 9
↑ソートされたリストをマージしたので、結果の順番もソートされている

list2
list4
↑追加した方はからになっている


7章：STL

9. queue

    queueはすでに説明したように、コンテナの最後にデータを追加し、コンテナの最初から 取得する構造です。

パフォーマンス(標準のqueueの場合)
作業内容 | 処理速度
要素の追加 | 定数時間(要素数によらない)
要素の削除 | 定数時間(要素数によらない)
ランダムアクセス | 不可能

queueクラスの主要な関数
関数名 | 書　式 | 説　明
empty | bool empty() const() | queueの要素が0なら「true」を、カラでなければ「false」を返します
size | size_type size()() | 要素数を返します
push | void push( const T &value ) | queueの末尾にデータを追加します
pop | void pop() | queueの先頭の要素を削除します
front | reference front()() | queueの先端の要素を返します
back | reference back()() | queueの末端の要素を返します

コンストラクタ
書　式 説　明
queue(const A& al = A())
例) queue<int> q();
queueを作成しqとする

    queueコンテナは、FIFOのキューとしてのアクセスしかできません。イタレーターも しようできないため、ごく限られた方法(front関数, back関数)でしか、データにアクセスすることができません。

    STLのくせに、なぜイタレーターすら使用できないのでしょう？　queueやpriority_queueの 中身は実はdequeコンテナなのです。dequeコンテナを使用して 目的にあわせたコンテナを作成しています。つまりdequeの機能の一部を提供している 特別なコンテナです。つまりイタレーターをサポートすると目的とするコンテナ以外の 操作が可能になってしまうため、イタレーターすら利用できないようにしているのです。 このようなコンテナを「アダプタ」と呼びます。

    queueの中身は実はdequeであることは上述の通りです。一般にqueueを利用する 場合は、dequeを利用するのが効率的なのですが、deque以外のコンテナを利用して queueを作成することもできます。下の例は、queueの中身としてlistを使用した例です。
queue< int, list<int> >

    「>」が２つ続いていますが、「>」と「>」の間はスペースかタブもしくは改行を入れないと エラーになりますので注意してください。続けてしまうと「>>」演算子と勘違いし、 コンパイルエラーになってしまう。

    次のページでは実際に使ってみます。


7章：STL

#include<iostream>
#include<queue>
using namespace std;

queue<int>  q;

void main(){
    // queueに追加
    for( int i = 0 ; i < 10 ; i++ )
        q.push( i );

    // queueの中身を表示
    while( !q.empty() ){
        cout << q.front() << " ";
        q.pop();
    }

    cout << endl << endl;

    // 1を追加する
    q.push( 1 );

    // 参照が返るので、代入することも可能
    q.back()  = 2;

    // 追加した「1」ではなく、代入した「2」が表示される
    cout << q.front() << endl;

    return;
}

0 1 2 3 4 5 6 7 8 9

2


7章：STL

10. deque

    dequeはすでに説明したように、コンテナの両端への追加および削除が頻繁に行われる 操作に向いています。また、ランダムアクセスも可能で、定数時間でアクセス可能です。

パフォーマンス
作業内容 | 処理速度
末端要素の追加 | 定数時間(要素数によらない)
末端要素の削除 | 定数時間(要素数によらない)
中間要素の追加 | 線形時間(要素数に比例)
中間要素の削除 | 線形時間(要素数に比例)
ランダムアクセス | 定数時間(要素数によらない)

dequeクラスの主要な関数
関数名 | 書　式 | 説　明
begin | iterator begin() | 先頭を表わすイタレーターを返します
end iterator begin() 末尾を表わすイタレーターを返します
末尾とは最後の要素の次を表わします
rbegin reverse_iterator rbegin() 末尾を表わす逆方向イタレーターを返します
末尾とは最後の要素の次を表わします
rend | reverse_iterator rend() | 先頭を表わす逆方向イタレーターを返します
empty | bool empty() const() | queueの要素が0なら「true」を、カラでなければ「false」を返します
size | size_type size()() | 要素数を返します
push_back | void push_back( const T &value ) | dequeの末尾にデータを追加します
push_front | void push_front( const T &value ) | dequeの先頭にデータを追加します
pop_back | void pop_back() | dequeの末尾の要素を削除します
pop_front | void pop_front() | dequeの先頭の要素を削除します
insert | iterator insert( iterator itr, const T &value ) | itrの直前にvalueを挿入します
insert | iterator insert( iterator itr, size_t num, const T &value ) | itrの直前にvalueをnum個挿入します
insert | iterator insert( iterator itr, const_iterator first, const_iterator last ) | itrの直前にfirstからlastの間のデータをコピーして挿入します
erase | iterator erase( iterator itr ) | itrの示す要素を削除します
erase | iterator erase( iterator start, iterator end ) | startからendまでの要素を削除します
clear | void clear() | すべての要素を削除します
front | reference front() | queueの先端の要素を返します
back | reference back() | queueの末端の要素を返します
at | reference at( size_type pos ) | pos番目の要素を返します
operator[] reference operator[]( size_type pos ) pos番目の要素を返します
(できるだけ「at」を使用することをお薦めします)
assign | void assign( const iterator first, const iterator last ) | deque(自分自身)の内容をfirstからlastに置き換えます
assign | void assign( size_type n, const T& x = T() ) | deque(自分自身)の内容をn個のxに置き換えます
swap | void swap( deque x ) | deque(自分自身)の内容をxと交換します

コンストラクタ
書　式 説　明
deque(const A& al = A())
例) deque<int> dq1;
dequeを作成しdq1とする
deque(size_type n, const T& v = T(), const A& al = A())
例) deque<int> dq2(5); 		n個のvからなるdequeを作成とする
要素数5のint型dequeを作成しdq2とする
deque(const deque& x)
例) deque<int> dq3(dq1);
dq1の複製を作成し、dq3とする
deque(const iterator first, const iterator last, const A& al = A())
例) deque<int> dq4(first, last);
firstからlastまでの要素のコピーを持つdequeを作成し、dq4とする

    dequeは、listとvectorのメンバー関数を備えたコンテナです。

    dequeでは、両端の要素の削除を行う場合は、削除された要素を指すイタレーターのみが 無効になります。しかし、要素の追加または、末端以外の削除を行うと、それまでの全ての イタレーターが無効になります。

    次のページでは実際に使ってみます。


7章：STL

    dequeを使った例を示します。main関数で２つのスレッドを起動しています。１つは キーボードから文字を受け付け１行入力されたら、それをstring型に変換して、dequeに 溜めます。２つめのスレッドはdequeを監視し、データが溜められたらそれを画面に 表示しています。

    このサンプルでは、stringクラスを使用しています。このstringクラスの詳細は 後述しますので、ここでは文字列を扱うクラスと考えてください。

    マルチスレッドでは、同時に複数のスレッドがdequeを参照します。この例では dequeに追加するスレッドと取り出すスレッドが同時にdequeを参照する可能性があります。 これは危険なことです。というのは、dequeはデータを追加するとすべてのイタレーターが 無効になりますので、２つのスレッドが同時にdequeを参照すると、エラーを起こす可能性が あるからです。本来ならばdequeをセマフォでロックし、２つのスレッドが同時にdequeを 参照しないようにするべきです。または、２つのスレッドが同時に参照しても良いように listを使うべきです。

#include<iostream>
#include<process.h>         // for _beginthread
#include<string>            // for basic_string
#include<queue>             // for deque

using namespace std;        // for STL

#define LINE_MAX    100     // buffer size

deque<string>   dq;         // deque instance
                            // --warning-- thread unsafe

bool            bContinue;  // thread flag( true  : thread continue )
                            //            ( false : thread end )

//
// 標準入力から文字列を読み込んで、dequeに詰める
void send_thread( void* arg ){
    // 文字を一時格納するバッファ
    char buffer[LINE_MAX];

    // 1行入力
    while( cin.getline( buffer, LINE_MAX ) )
        // dequeに詰める
        dq.push_back( buffer );

    // 入力が終わりなら、スレッドフラグをfalseにする
    bContinue = false;

    // receive_threadを起こす
    dq.push_back( "" );
}

//
// dequeに文字列が溜まれば、画面に出力する
void receive_thread( void* arg ){
    // スレッドフラグがtrueの間繰り返す
    while( bContinue ){
        // dequeに文字列が溜まるのを待つ
        while( dq.empty() );

        // 溜まった文字列を出力する
        cout << dq.front() << endl;

        // 出力した文字列を削除する
        dq.pop_front();
    }
}

// send_threadとreceive_threadを起動する
void main(){
    // スレッドフラグをtrueにする
    bContinue = true;

    // send_threadを起動する
    _beginthread( send_thread, 0, 0 );

    // receive_threadを起動する
    _beginthread( receive_thread, 0, 0 );

    // スレッドが終わるのを待つ
    while( bContinue );
}

abcdef
abcdef
123345
123345
this is a pen.
this is a pen.


7章：STL

11. priority_queue

    priority_queueはすでに説明したように、優先順位を持つキューです。要素を使いする場合、 優先順位によって並べ替えられます。

    priority_queueもqueueと同様、アダプタです。つまりある別のコンテナを利用して実現され、 そのコンテナの機能を一部分だけ公開したものです。priority_queueは標準でvectorコンテナを 利用しています。

priority_queueクラスの主要な関数
関数名 | 書　式 | 説　明
empty | bool empty() const() | queueの要素が0なら「true」を、カラでなければ「false」を返します
size | size_type size()() | 要素数を返します
push | void push( const T &value ) | priority_queueの末尾にデータを追加します
pop | void pop() | priority_queueの先頭の要素を削除します
front | reference top() | priority_queueの先端の要素を返します

コンストラクタ
書　式 説　明
priority_queue(const Pred& pr = Pred(),const A& al = A())
例) deque<int> pq1;
priority_queueを作成しpq1とする
priority_queue(InItr first, InItr last,const Pred& pr = Pred(),const A& al = A())
例) deque<int> pq2(first, last);
priority_queueを作成しpq2とし、firstからlastまでの要素を追加する

    次のページでは実際に使ってみます。


7章：STL

    簡単なpriority_queueの使い方を示します。int型のpriority_queueを作成し、乱数を 格納します。その後、すべての要素を表示します。

#include<iostream>
#include<queue>
using namespace std;

void main(){
    // 空のint型priority_queueを作る
    priority_queue<int>  pq;

    int  i;

    // priority_queueに数字を代入する
    for( i = 0 ; i < 10 ; i++ )
        pq.push( rand()%10 );

    // priority_queueの中身を表示する
    while( !pq.empty() ){
        cout << pq.top() << ' ';
        pq.pop();
    }
    cout << endl;
}

9 8 8 7 4 4 4 2 1 0
    降順ソートされて結果が表示されているのがわかります。つまり優先順位の高い方から 並べ替えられているのです。

    優先順位の比較には「operator<(const T&a, const T&b)」が 呼び出されます。したがってクラス型のpriority_queueを作成する場合には、このオペレーターを 実装する必要があります。


7章：STL

    優先順位の比較に使われるオペレーターを変えたい場合も当然あると思います。 その場合には以下のようにpriority_queueを定義します。
関数名 定　義
operaotr<
priority_queue<int, vector<int, less<int> >
operaotr&>
priority_queue<int, vector<int, greater<int> >
ユーザー指定関数
template<class T>
struct func : binary_function<T, T, bool> {
    bool operator()(const T& X, const T& Y) const{
        return (X < Y);    // ←ここを書き換える
    }
};

priority_queue<int, vector<int, func<int> >
    次の例では優先順位を逆順にする例です。

#include<iostream>
#include<queue>
using namespace std;

template<class T>
struct func : binary_function<T, T, bool> {
    bool operator()(const T& X, const T& Y) const{
        return (X > Y);
    }
};

void main(){
    // 空のint型priority_queueを作る
    priority_queue<int, vector<int>, func<int> >  pq;
//  priority_queue<int, vector<int>, greater<int> >  pq;    // ←でも同じ

    int  i;

    // priority_queueに数字を代入する
    for( i = 0 ; i < 10 ; i++ )
        pq.push( rand()%10 );

    // priority_queueの中身を表示する
    while( !pq.empty() ){
        cout << pq.top() << ' ';
        pq.pop();
    }
    cout << endl;
}

0 1 2 4 4 4 7 8 8 9
    昇順にソートされながらデータが追加されているのがわかると思います。

    ただし、自分で関数を作り(上の例ではfunc)、それを用いて優先順位を決定するのは あまり推奨できません。できるだけoperator<(less)かoperator>(greater)を指定するべきです。


7章：STL

    次に、クラス型のpriority_queueを使った例を示します。
#include<iostream>
#include<queue>
#include<string>
using namespace std;

// イベントクラス
class Event{
public:
    Event(string msg, int priority){
        m_msg      = msg;
        m_priority = priority;
    }

    string m_msg;
    int    m_priority;
};

// 比較演算子
bool operator<( const Event& a, const Event& b ){
    return a.m_priority < b.m_priority;
}

// 出力ストリーム
ostream& operator<<( ostream& os, const Event& event ){
    return os << event.m_priority << "  " << event.m_msg << endl;
}

void main(){
    // 空のint型priority_queueを作る
    priority_queue<Event, vector<Event>, less<Event> >  pq;

    pq.push( Event("火事だ!", 10) );
    pq.push( Event("地震だ!", 9 ) );
    pq.push( Event("電話です", 3) );
    pq.push( Event("誰か来ました!", 4) );
    pq.push( Event("メールです", 2) );

    // priority_queueの中身を表示する
    while( !pq.empty() ){
        cout << pq.top() << ' ';
        pq.pop();
    }
    cout << endl;
}

10  火事だ!
 9  地震だ!
 4  誰か来ました!
 3  電話です
 2  メールです


7章：STL

12. stack

    stackはすでに説明したように、コンテナの最後にデータを追加し、コンテナの最後から 取得する構造です。

    stackもアダプタで、実際のコンテナは標準でdequeが 使われています。

stackクラスの主要な関数
関数名 | 書　式 | 説　明
empty | bool empty() const() | queueの要素が0なら「true」を、カラでなければ「false」を返します
size | size_type size()() | 要素数を返します
push | void push( const T &value ) | stackの末尾にデータを追加します
pop | void pop() | stackの先頭の要素を削除します
top | reference top() | queueの先端の要素を返します

コンストラクタ
書　式 説　明
stack(A& al = A())
例) stack<int> st();
stackを作成しstとする

    stackの中身は実はdequeであることは上述の通りです。一般にstackを利用する 場合は、dequeを利用するのが効率的なのですが、deque以外のコンテナを利用して stackを作成することもできます。下の例は、stackの中身としてvectorを使用した例です。
stack< int, vector<stack> >

    「>」が２つ続いていますが、「>」と「>」の間はスペースかタブもしくは改行を入れないと エラーになりますので注意してください。続けてしまうと「>>」演算子と勘違いし、 コンパイルエラーになってしまう。

    次のページでは実際に使ってみます。


7章：STL

    stackを使った例を示します。

#include<iostream>
#include<stack>
using namespace std;

void main(){
    int i;

    // 空のint型stackを作る
    stack<int>	st;

    for( i = 0 ; i < 5 ; i++ ){
        st.push( i );
        cout << "push : " << i << endl;
    }

    cout << endl;

    while( !st.empty() ){
        cout << "pop : " << st.top() << endl;
        st.pop();
    }
}

push : 0
push : 1
push : 2
push : 3
push : 4

pop : 4
pop : 3
pop : 2
pop : 1
pop : 0
    逆ポーランド電卓の例を示します。
#include <stdlib.h> // atof
#include <string.h> // strcmp
#include <iostream.h>
#include <stack>
using namespace std;

////////////////////////////////////////////////////////////////////////////
// メイン

int main( int argc, char** argv ){
    double value;
    double operand1;
    double operand2;

    // スタックの作成
    stack<double> ostack;

    // コマンド行引数からリバーシュポーリッシュ記法で記述された式を
    // 読み込み計算する
    for( int i = 1; i < argc; i++){
        // 加法
        if( strcmp( argv[i], "+" ) == 0 ){
            // ope2の取得
            operand2 = ostack.top();
            ostack.pop();

            // ope1の取得
            operand1 = ostack.top();
            ostack.pop();

            // 答え格納
            ostack.push( operand1 + operand2 );
        }

        // 減法
        else if( strcmp( argv[i], "-" ) == 0 ){
            operand2 = ostack.top();
            ostack.pop();

            operand1 = ostack.top();
            ostack.pop();

            ostack.push( operand1 - operand2 );
        }

        // 乗法
        else if( strcmp( argv[i], "*" ) == 0 ){
            operand2 = ostack.top();
            ostack.pop();

            operand1 = ostack.top();
            ostack.pop();

            ostack.push( operand1 * operand2 );
        }

        // 除法
        else if( strcmp( argv[i], "/" ) == 0 ){
            operand2 = ostack.top();
            ostack.pop();

            operand1 = ostack.top();
            ostack.pop();

            // 0除算のチェック
            if( operand2 == 0.0 ){
                cerr << "divid by 0" << endl;
                return 1;
            }

            ostack.push( operand1 / operand2 );
        }

        // 上記以外は実数を仮定する
        else{
            value = atof( argv[i] );
            ostack.push( value );
        }
    }

    // スタックの整合性の確認
    if( ostack.size() != 1 ){
        cerr << "illegal expression" << endl;
        return 1;
    }

    // 答を出力する
    value = ostack.top();
    ostack.pop();

    cout << value << endl;

    return 0;
}

C:\>rpexpr 10 20 +
30

C:\>rpexpr 10 20 + 3 /
10

C:\>rpexpr 1 2 3 4 5 6 7 8 9 10 + + + + + + + + +
55

C:\>rpexpr 1 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +
55

C:\>


7章：STL

13. map

    mapはすでに説明したように、表や辞書を作るのに適したコンテナです。キーと呼ばれる データと、それから検索される値というデータの２つを格納します。

    例えば、英和辞書ならば、キーは英単語、値は日本語訳になります。

    これまでのコンテナのように、直接データを入れるのではなく、mapクラスでは、 pair構造体のメンバー変数として、キーと値を代入し、それをmapコンテナに追加するという 方法になります。

パフォーマンス
作業内容 | 処理速度
要素の追加 | 対数時間
要素の削除 | 対数時間
要素の追加(追加場所をイタレーターで指定) | 定数時間
要素の削除(イタレーターで指定) | 定数時間

mapクラスの主要な関数
関数名 | 書　式 | 説　明
empty | bool empty() const() | queueの要素が0なら「true」を、カラでなければ「false」を返します
size | size_type size()() | 要素数を返します
count | size_type count(const key_type& key) const() | keyに一致するキーの数(0または1)を返します。
insert | pair<iterator, bool> insert( const value_type &x ) | xを追加します。戻り値として、結果が帰ります。
insert | iterator insert( iterator it, const value_type& x ) | itの直後にxを追加し、イタレーターを返します。
insert | void insert(const value_type* first, const value_type* last) | firstからlastまでの要素を追加します。
find | iterator find(const key_type& key) | keyを検索し、その値を指すイタレーターを返します。keyが見つからない場合は最後を示すイタレーターを返します。
clear | void clear() | すべての要素を削除します。
erase | void erase(iterator it ) | itが指す要素を削除します。
erase | void erase(iterator start, iterator last ) | startからlastまでの要素を削除します。
erase | size_type erase(const key_type key ) | keyを削除します。削除した項目数(0または1)を返します。
begin | iterator begin() | マップの最初の要素を指すイタレーターを返します
end | iterator end() | マップの末尾を指すイタレーターを返します
rbegin | iterator begin() | マップの末尾を指す逆方向イタレーターを返します
rend | iterator end() | マップの先頭を指す逆方向イタレーターを返します
operator[] | reference operator[](const key_type& key) | keyを検索し、その値を指す値の参照を返します。
upper_bound iterator upper_bound(const key_type& key) keyより大きな要素(pair)の最初を指すイタレーターを返します。
存在しない場合は末尾を指すイタレーターを返します。
lower_bound iterator lower_bound(const key_type& key) key以下の要素(pair)の最後を指すイタレーターを返します。
存在しない場合は末尾を指すイタレーターを返します。
equal_range | pair<iterator, iterator> equal_range(const key_type& key) | このメンバ関数は、x.first == lower_bound(key) および x.second == upper_bound(key) とする反復子 x のペアを返します。
※value_typeはpair型を示します。
※いくつかのメンバー関数は、multimapとの互換を取るために 用意されているため、mapでは無意味なものも用意されている。

iterator upper_bound(const Key& key); コンストラクタ
書　式 説　明
map(const Pred& comp = Pred(), const A& al = A())
例) map<char, int> m1();
charからintを検索するmapを作成しm1とする
map(const map& x)
例) map<char, int> m2(m1); 		コピーコンストラクタ
m1と同じmapを作成し、m2とする
map(const value_type* first, const value_type*
last, const Pred& comp = Pred(), const A& al = A()))
例) map<char, int> m3(m1.begin(), m1.end());

m1の先頭から末尾までの要素からなるmapを作成し、m3とする

    次のページでは実際に使ってみます。


7章：STL

    mapを使った例を示します。
// STLを使うとwarningが出る可能性があるので、それを出さないようにする
#pragma warning (disable:4786)

#include<map>
#include<iostream.h>
using namespace std;

void main(){
    // mapを宣言
    map<char, int> m;

    int     i;
    char    ch;

    // データを追加
    for( i = 'a' ; i <= 'z' ; i++ )
        m.insert( pair<char, int>((char)i, i) );

    while( cout << endl << "Enter Key : " << flush, cin >> ch ){
        // map型イタレーターの宣言
        map<char, int>::iterator itr;

        // 検索
        itr = m.find(ch);

        if( itr == m.end() )
            // 見つからなかった？
            cout << "No entry!!" << endl;
        else
            // 見つかった
            // イタレーターはpair型を指している
            // itr->second が値を保持している
            // itr->first  がキーを保持している
            cout << "ASCII Code : " << itr->second << endl;
    }
}

Enter Key : 1
No entry!!

Enter Key : a
ASCII Code : 97

Enter Key : c
ASCII Code : 99

Enter Key : z
ASCII Code : 122

Enter Key : ^Z

    イタレーターはpair型を指しています。したがって値を得たい場合は 「itr->second 」とし、キーを得たい場合は「itr->first」 とします。

    次に、イタレーターを使用して、すべての要素を巡回する例を示します。
// STLを使うとwarningが出る可能性があるので、それを出さないようにする
#pragma warning (disable:4786)

#include<map>
#include<iostream.h>
using namespace std;

void main(){
    // mapを宣言
    map<char, int> m;

    int  i;

    // データを降順に追加
    for( i = 'z' ; i >= 'a' ; i-- )
        m.insert( pair<char, int>((char)i, i) );

    map<char, int>::iterator itr    = m.begin();
    map<char, int>::iterator itrEnd = m.end();

    // 要素をすべて表示
    for( ; itr != itrEnd ; itr++ )
        cout << "Key : " << itr->first << ",   Value : " << itr->second << endl;
}

Key : a,   Value : 97
Key : b,   Value : 98
Key : c,   Value : 99
Key : d,   Value : 100
Key : e,   Value : 101
Key : f,   Value : 102
Key : g,   Value : 103
Key : h,   Value : 104
Key : i,   Value : 105
Key : j,   Value : 106
Key : k,   Value : 107
Key : l,   Value : 108
Key : m,   Value : 109
Key : n,   Value : 110
Key : o,   Value : 111
Key : p,   Value : 112
Key : q,   Value : 113
Key : r,   Value : 114
Key : s,   Value : 115
Key : t,   Value : 116
Key : u,   Value : 117
Key : v,   Value : 118
Key : w,   Value : 119
Key : x,   Value : 120
Key : y,   Value : 121
Key : z,   Value : 122

    データを'z'から'a'まで降順に挿入しているにもかかわらず、昇順にデータが追加されています。 これは検索する際に、効率良く検索できるようにするため、要素を追加する際に昇順にソートしながら データを追加しているためです。

    最後に「operator[]」について説明します。
// STLを使うとwarningが出る可能性があるので、それを出さないようにする
#pragma warning (disable:4786)

#include<map>
#include<iostream.h>
using namespace std;

void main(){
    // mapを宣言
    map<char, int> m;

    int  i;

    // データを追加
    for( i = '0' ; i <= '9' ; i++ )
        m[(char)i] = i;

    // 登録していないものを取得しようとしたら・・・。
    // m['a'] = 0;
    // int j = m['a'];
    // と同じ
    int j = m['a'];

    map<char, int>::iterator itr    = m.begin();
    map<char, int>::iterator itrEnd = m.end();

    // 要素をすべて表示
    for( ; itr != itrEnd ; itr++ )
        cout << "Key : " << itr->first << ",   Value : " << itr->second << endl;
}

Key : 0,   Value : 48
Key : 1,   Value : 49
Key : 2,   Value : 50
Key : 3,   Value : 51
Key : 4,   Value : 52
Key : 5,   Value : 53
Key : 6,   Value : 54
Key : 7,   Value : 55
Key : 8,   Value : 56
Key : 9,   Value : 57
Key : a,   Value : 0

    「operator[]」はキーを検索し、その値を取得するものです。これは 検索するキーが必ず登録されていることが前提になっています。もし、登録されていない キーを検索した場合は、そのキーを登録します。このときの値は、デフォルトコンストラクタが 呼ばれます。intやchar、doubleのような標準の型は0が代入され、boolであればflaseが 代入されます。

    登録されていないキーは自動的に登録されるということは、このオペレーターを利用して データの追加をすることができます。ただし、デフォルトコンストラクタが呼ばれた後に、 代入演算子を利用して代入されるため、効率が悪くなります。できるだけ、inser関数を 使うべきです。

    mapは、キーの重複はできません。すでに登録されているキーに対して、insertを行っても データの追加はおろか、変更もできません。

    すでに登録されているキーの値を変更したい場合は、operator[]を使います。

// STLを使うとwarningが出る可能性があるので、それを出さないようにする
#pragma warning (disable:4786)

#include<map>
#include<iostream.h>
using namespace std;

void main(){
    // mapを宣言
    map<char, int> m;

    // 最初のデータ追加
    m['0'] = 10;

    // insertを使ったデータの追加
    // 追加も変更もされない
    map<char, int>::iterator itr;
    itr = m.insert(m.begin(), pair<char, int>('0', 20));
    cout << "Key : " << itr->first << ",   Value : " << itr->second << endl;

    // insertを使ったデータの追加
    // 追加も変更もされない
    pair<map<char, int>::iterator, bool> p;
    p = m.insert(pair<char, int>('0', 30));
    cout << "Key : " << (*(p.first)).first << ",   Value : " << (*(p.first)).second << endl;

    // operator[]を使ったデータ変更
    // 変更はできる
    m['0'] = 40;
    cout << "Key : " << 0 << ",   Value : " << m['0'] << endl;
}

Key : 0,   Value : 10
Key : 0,   Value : 10
Key : 0,   Value : 40


7章：STL

    mapに自分で作成したクラスを格納する場合、キーはソートをするために必要な operator<を実装する必要があります。ただしコンパイラによっては 他のオペレーターを実装する必要があります。

    また、キーに対応づける値は、デフォルトコンストラクタを実装する必要があります。

// STLを使うとwarningが出る可能性があるので、それを出さないようにする
#pragma warning (disable:4786)

#include<map>
#include<iostream.h>
#include <iomanip.h>

// 4-19で作成したstring.h
#include"string.h"

using namespace std;

void main(){
    // mapを宣言
    map<String, String> m;

    // 映画のタイトルと、監督をマップに追加
    m.insert(pair<String, String>("Six Days Seven Night", "Ivan Reitman"));
    m.insert(pair<String, String>("Snow White", "Michael Cohn"));
    m.insert(pair<String, String>("Volcano", "Mick Jackson"));
    m.insert(pair<String, String>("End of Days", "Peter Hyams"));
    m.insert(pair<String, String>("The Enemy of States", "Tony Scot"));
    m.insert(pair<String, String>("Scream", "Wes Craven"));

    cout << setiosflags( ios::left ) << flush;
    cout << setw(30) << "Movie Title" << setw(20) << "Director" << endl << endl;

    map<String, String>::iterator itr    = m.begin();
    map<String, String>::iterator itrEnd = m.end();

    // 表示
    for( ; itr != itrEnd ; itr++ )
        cout << setw(30) << itr->first << setw(20) << itr->second << endl;
}

Movie Title                   Director

End of Days                   Peter Hyams
Scream                        Wes Craven
Six Days Seven Night          Ivan Reitman
Snow White                    Michael Cohn
The Enemy of States           Tony Scot
Volcano                       Mick Jackson

    デフォルトではinsert関数は昇順になるようにキーを追加していきます。これを降順に したい場合は以下のようにします。
// STLを使うとwarningが出る可能性があるので、それを出さないようにする
#pragma warning (disable:4786)

#include<map>
#include<iostream.h>
#include <iomanip.h>

// 4-19で作成したstring.h
#include"string.h"

using namespace std;

void main(){
    // mapを宣言
    map<String, String, greater<String> > m;

    // 映画のタイトルと、監督をマップに追加
    m.insert(pair<String, String>("Six Days Seven Night", "Ivan Reitman"));
    m.insert(pair<String, String>("Snow White", "Michael Cohn"));
    m.insert(pair<String, String>("Volcano", "Mick Jackson"));
    m.insert(pair<String, String>("End of Days", "Peter Hyams"));
    m.insert(pair<String, String>("The Enemy of States", "Tony Scot"));
    m.insert(pair<String, String>("Scream", "Wes Craven"));

    cout << setiosflags( ios::left ) << flush;
    cout << setw(30) << "Movie Title" << setw(20) << "Director" << endl << endl;

    map<String, String, greater<String> >::iterator itr    = m.begin();
    map<String, String, greater<String> >::iterator itrEnd = m.end();

    // 表示
    for( ; itr != itrEnd ; itr++ )
        cout << setw(30) << itr->first << setw(20) << itr->second << endl;
}

Movie Title                   Director

Volcano                       Mick Jackson
The Enemy of States           Tony Scot
Snow White                    Michael Cohn
Six Days Seven Night          Ivan Reitman
Scream                        Wes Craven
End of Days                   Peter Hyams

    最後にlower_boundとupper_boundの使い方の例です。
// STLを使うとwarningが出る可能性があるので、それを出さないようにする
#pragma warning (disable:4786)

#include<map>
#include<iostream.h>
#include <iomanip.h>

// 4-19で作成したstring.h
#include"string.h"

using namespace std;

void main(){
    // mapを宣言
    map<String, String> m;

    // 映画のタイトルと、監督をマップに追加
    m.insert(pair<String, String>("Six Days Seven Night", "Ivan Reitman"));
    m.insert(pair<String, String>("Snow White", "Michael Cohn"));
    m.insert(pair<String, String>("Volcano", "Mick Jackson"));
    m.insert(pair<String, String>("End of Days", "Peter Hyams"));
    m.insert(pair<String, String>("The Enemy of States", "Tony Scot"));
    m.insert(pair<String, String>("Scream", "Wes Craven"));

    cout << setiosflags( ios::left ) << flush;
    cout << setw(30) << "Movie Title" << setw(20) << "Director" << endl << endl;

    map<String, String> >::iterator itr = m.lower_bound("Scream");
    map<String, String> >::iterator itrEnd = m.end();

    // 表示
    for( ; itr != itrEnd ; itr++ )
        cout << setw(30) << itr->first << setw(20) << itr->second << endl;

    cout << endl;

    itr = m.upper_bound("Scream");
    // 表示
    for( ; itr != itrEnd ; itr++ )
        cout << setw(30) << itr->first << setw(20) << itr->second << endl;

}

Movie Title                   Director

Scream                        Wes Craven
Six Days Seven Night          Ivan Reitman
Snow White                    Michael Cohn
The Enemy of States           Tony Scot
Volcano                       Mick Jackson

Six Days Seven Night          Ivan Reitman
Snow White                    Michael Cohn
The Enemy of States           Tony Scot
Volcano                       Mick Jackson


7章：STL

14. multimap

    multimapはすでに説明したように、キーの重複が可能なマップです。

パフォーマンス
作業内容 | 処理速度
要素の追加 | 対数時間
要素の削除 | 対数時間
要素の追加(追加場所をイタレーターで指定) | 定数時間
要素の削除(イタレーターで指定) | 定数時間

mapクラスの主要な関数
関数名 | 書　式 | 説　明
empty | bool empty() const() | queueの要素が0なら「true」を、カラでなければ「false」を返します
size | size_type size()() | 要素数を返します
count | size_type count(const key_type& key) const() | keyに一致するキーの数を返します。
insert | pair<iterator, bool> insert( const value_type &x ) | xを追加します。戻り値として、結果が帰ります。
insert | iterator insert( iterator it, const value_type& x ) | itの直後にxを追加し、イタレーターを返します。
insert | void insert(const value_type* first, const value_type* last) | firstからlastまでの要素を追加します。
find | iterator find(const key_type& key) | keyを検索し、その値を指すイタレーターを返します。keyが見つからない場合は最後を示すイタレーターを返します。
clear | void clear() | すべての要素を削除します。
erase | void erase(iterator it ) | itが指す要素を削除します。
erase | void erase(iterator start, iterator last ) | startからlastまでの要素を削除します。
erase | size_type erase(const key_type key ) | keyを削除します。削除した項目数を返します。
begin | iterator begin() | マップの最初の要素を指すイタレーターを返します
end | iterator end() | マップの末尾を指すイタレーターを返します
rbegin | iterator begin() | マップの末尾を指す逆方向イタレーターを返します
rend | iterator end() | マップの先頭を指す逆方向イタレーターを返します
operator[] | reference operator[](const key_type& key) | keyを検索し、その値を指す値の参照を返します。
upper_bound iterator upper_bound(const key_type& key) keyより大きな要素(pair)の最初を指すイタレーターを返します。
存在しない場合は末尾を指すイタレーターを返します。
lower_bound iterator lower_bound(const key_type& key) key以下の要素(pair)の最後を指すイタレーターを返します。
存在しない場合は末尾を指すイタレーターを返します。
equal_range | pair<iterator, iterator> equal_range(const key_type& key) | このメンバ関数は、x.first == lower_bound(key) および x.second == upper_bound(key) とする反復子 x のペアを返します。
※value_typeはpair型を示します。

コンストラクタ
書　式 説　明
multimap(const Pred& comp = Pred(), const A& al = A())
例) multimap<char, int> m1();
charからintを検索するmultimapを作成しm1とする
multimap(const multimap& x)
例) multimap<char, int> m2(m1); 		コピーコンストラクタ
m1と同じmultimapを作成し、m2とする
multimap(const value_type* first, const value_type*
last, const Pred& comp = Pred(), const A& al = A()))
例) multimap<char, int> m3(m1.begin(), m1.end());

m1の先頭から末尾までの要素からなるmultimapを作成し、m3とする

    次のページでは実際に使ってみます。


7章：STL

    multimapを使った例を示します。
// STLを使うとwarningが出る可能性があるので、それを出さないようにする
#pragma warning (disable:4786)

#include<map>
#include<iostream.h>
#include <iomanip.h>
#include"string.h"

using namespace std;

void main(){
    cout << setiosflags( ios::left ) << flush;

    // multimapを宣言
    multimap<String, String> m;

    // データの追加
    m.insert(pair<String, String>("Ishibashi", "大型２種自動車免許"));
    m.insert(pair<String, String>("Ishibashi", "デジタル３種工事担任者"));

    m.insert(pair<String, String>("Sekiguchi", "第２種情報処理技術者"));
    m.insert(pair<String, String>("Sekiguchi", "第１種情報処理技術者"));
    m.insert(pair<String, String>("Sekiguchi", "甲種危険物取扱者"));
    m.insert(pair<String, String>("Sekiguchi", "毒物劇物取り扱い技術者"));

    m.insert(pair<String, String>("Tsutsui", "第４級アマチュア無線技士"));

    multimap<String, String>::iterator itr    = m.begin();
    multimap<String, String>::iterator itrEnd = m.end();

    // まずはすべて表示
    for( ; itr != itrEnd ; itr++ )
        cout << setw(20) << itr->first << setw(30) << itr->second << endl;

    cout << endl << endl;

    // Sekiguchiのデータだけを表示
    pair<multimap<String, String>::iterator, multimap<String, String>::iterator> p = m.equal_range("Sekiguchi");
    for( ; p.first != p.second ; p.first++ )
        cout << setw(20) << (p.first)->first << setw(30) << (p.first)->second << endl;
}

Ishibashi           大型２種自動車免許
Ishibashi           デジタル３種工事担任者
Sekiguchi           第２種情報処理技術者
Sekiguchi           第１種情報処理技術者
Sekiguchi           甲種危険物取扱者
Sekiguchi           毒物劇物取り扱い技術者
Tsutsui             第４級アマチュア無線技士

Sekiguchi           第２種情報処理技術者
Sekiguchi           第１種情報処理技術者
Sekiguchi           甲種危険物取扱者
Sekiguchi           毒物劇物取り扱い技術者

    上記のように、あるキーに関連づけられている値をすべて検索したい場合は 「equal_range」関数を用いる。


7章：STL

15. set

    setはすでに説明したように、ソートしながらデータを追加するコンテナです。ただし、 データの重複は許されません。

パフォーマンス
作業内容 | 処理速度
要素の追加 | 対数時間
要素の削除 | 対数時間
要素の追加(追加場所をイタレーターで指定) | 定数時間
要素の削除(イタレーターで指定) | 定数時間

setクラスの主要な関数
関数名 | 書　式 | 説　明
empty | bool empty() const() | queueの要素が0なら「true」を、カラでなければ「false」を返します
size | size_type size()() | 要素数を返します
count | size_type count(const key_type& key) const() | keyに一致するキーの数(0または1)を返します。
insert | pair<iterator, bool> insert( const value_type &x ) | xを追加します。戻り値として、結果が帰ります。
insert | iterator insert( iterator it, const value_type& x ) | itの直後にxを追加し、イタレーターを返します。
insert | void insert(const value_type* first, const value_type* last) | firstからlastまでの要素を追加します。
find | iterator find(const key_type& key) | keyを検索し、その値を指すイタレーターを返します。keyが見つからない場合は最後を示すイタレーターを返します。
clear | void clear() | すべての要素を削除します。
erase | void erase(iterator it ) | itが指す要素を削除します。
erase | void erase(iterator start, iterator last ) | startからlastまでの要素を削除します。
erase | size_type erase(const key_type key ) | keyを削除します。削除した項目数(0または1)を返します。
begin | iterator begin() | マップの最初の要素を指すイタレーターを返します
end | iterator end() | マップの末尾を指すイタレーターを返します
rbegin | iterator begin() | マップの末尾を指す逆方向イタレーターを返します
rend | iterator end() | マップの先頭を指す逆方向イタレーターを返します
upper_bound iterator upper_bound(const key_type& key) keyより大きな要素(pair)の最初を指すイタレーターを返します。
存在しない場合は末尾を指すイタレーターを返します。
lower_bound iterator lower_bound(const key_type& key) key以下の要素(pair)の最後を指すイタレーターを返します。
存在しない場合は末尾を指すイタレーターを返します。
equal_range | pair<iterator, iterator> equal_range(const key_type& key) | このメンバ関数は、x.first == lower_bound(key) および x.second == upper_bound(key) とする反復子 x のペアを返します。
※setでは、key_typeとvalue_typeは同じです。

コンストラクタ
書　式 説　明
set(const Pred& comp = Pred(), const A& al = A())
例) set<int> s1();
intを格納するsetを作成しs1とする
set(const set& x)
例) set<int> s2(s1); 		コピーコンストラクタ
m1と同じsetを作成し、s2とする
set(const value_type* first, const value_type*
last, const Pred& comp = Pred(), const A& al = A()))
例) set<int> s3(s1.begin(), s1.end());

s1の先頭から末尾までの要素からなるsetを作成し、s3とする

    次のページでは実際に使ってみます。


7章：STL

    setは、int型などの標準で用意されている型を使用した場合には、ただのソートしながら 追加するだけのコンテナです。setが便利なのは、自分で用意したクラスを使用する場合で、 なおかつ、operator<を使用した場合です。 　　ここではsetを使った名簿の例を示します。
// STLを使うとwarningが出る可能性があるので、それを出さないようにする
#pragma warning (disable:4786)

#include<set>
#include<iostream.h>
#include <iomanip.h>
#include"string.h"

using namespace std;

// 名簿クラス
class Meibo{
public:
    // デフォルトコンストラクタ
    Meibo() :
        GakuseiNo( -1 ),
        Name( "" )  {}
    // コンストラクタ
    Meibo(int n, String name) :
        GakuseiNo( n ),
        Name( name ){}

    // 比較演算子
    bool operator<(const Meibo& a)const{
        return GakuseiNo < a.GakuseiNo;
    }

    // メンバー変数
    int    GakuseiNo;
    String Name;
};

void main(){
    cout << setiosflags( ios::left ) << flush;

    // setを宣言
    set<Meibo> s;

    // データ追加
    s.insert(Meibo(25, "Hirozue Ryoko") );
    s.insert(Meibo( 2, "Abe Natsuko") );
    s.insert(Meibo(13, "Suzuki Ame") );
    s.insert(Meibo( 7, "Kubo Shizuka") );

    cout << setiosflags( ios::left ) << flush;

    // すべてを表示
    set<Meibo>::iterator itr    = s.begin();
    set<Meibo>::iterator itrEnd = s.end();
    for( ; itr != itrEnd ; itr++ )
        cout << setw(4) << itr->GakuseiNo << setw(20) << itr->Name << endl;
}

2   Abe Natsuko
7   Kubo Shizuka
13  Suzuki Ame
25  Hirozue Ryoko


7章：STL

16. multiset

    multisetはすでに説明したように、ソートしながらデータを追加するコンテナです。 データの重複も可能です。

パフォーマンス
作業内容 | 処理速度
要素の追加 | 対数時間
要素の削除 | 対数時間
要素の追加(追加場所をイタレーターで指定) | 定数時間
要素の削除(イタレーターで指定) | 定数時間

multisetクラスの主要な関数
関数名 | 書　式 | 説　明
empty | bool empty() const() | queueの要素が0なら「true」を、カラでなければ「false」を返します
size | size_type size()() | 要素数を返します
count | size_type count(const key_type& key) const() | keyに一致するキーの数を返します。
insert | pair<iterator, bool> insert( const value_type &x ) | xを追加します。戻り値として、結果が帰ります。
insert | iterator insert( iterator it, const value_type& x ) | itの直後にxを追加し、イタレーターを返します。
insert | void insert(const value_type* first, const value_type* last) | firstからlastまでの要素を追加します。
find | iterator find(const key_type& key) | keyを検索し、その値を指すイタレーターを返します。keyが見つからない場合は最後を示すイタレーターを返します。
clear | void clear() | すべての要素を削除します。
erase | void erase(iterator it ) | itが指す要素を削除します。
erase | void erase(iterator start, iterator last ) | startからlastまでの要素を削除します。
erase | size_type erase(const key_type key ) | keyを削除します。削除した項目数を返します。
begin | iterator begin() | マップの最初の要素を指すイタレーターを返します
end | iterator end() | マップの末尾を指すイタレーターを返します
rbegin | iterator begin() | マップの末尾を指す逆方向イタレーターを返します
rend | iterator end() | マップの先頭を指す逆方向イタレーターを返します
upper_bound iterator upper_bound(const key_type& key) keyより大きな要素(pair)の最初を指すイタレーターを返します。
存在しない場合は末尾を指すイタレーターを返します。
lower_bound iterator lower_bound(const key_type& key) key以下の要素(pair)の最後を指すイタレーターを返します。
存在しない場合は末尾を指すイタレーターを返します。
equal_range | pair<iterator, iterator> equal_range(const key_type& key) | このメンバ関数は、x.first == lower_bound(key) および x.second == upper_bound(key) とする反復子 x のペアを返します。
※setでは、key_typeとvalue_typeは同じです。

コンストラクタ
書　式 説　明
multiset(const Pred& comp = Pred(), const A& al = A())
例) multiset<int> s1();
intを格納するmultisetを作成しs1とする
multiset(const set& x)
例) multiset<int> s2(s1); 		コピーコンストラクタ
m1と同じmultisetを作成し、s2とする
multiset(const value_type* first, const value_type*
last, const Pred& comp = Pred(), const A& al = A()))
例) multiset<int> s3(s1.begin(), s1.end());

s1の先頭から末尾までの要素からなるmultisetを作成し、s3とする

    次のページでは実際に使ってみます。


7章：STL

    multisetは、int型などの標準で用意されている型を使用した場合には、ただのソートしながら 追加するだけのコンテナです。multisetが便利なのは、自分で用意したクラスを使用する場合で、 なおかつ、operator<を使用した場合です。

    ここではmultisetを使った名簿の例を示します。
// STLを使うとwarningが出る可能性があるので、それを出さないようにする
#pragma warning (disable:4786)

#include<set>
#include<iostream.h>
#include <iomanip.h>
#include"string.h"

using namespace std;

// 本クラス
class Book{
public:
    // デフォルトコンストラクタ
    Book() :
        Author( "" ),
        Title( "" )  {}
    // コンストラクタ
    Book(String author) :
        Author( author ),
        Title( "" ) {}
    // コンストラクタ
    Book(String author, String title) :
        Author( author ),
        Title( title ){}

    // 比較演算子
    bool operator<(const Book& a)const{
        return Author < a.Author;
    }

    // メンバー変数
    String Author;
    String Title;
};

void main(){
    cout << setiosflags( ios::left ) << flush;

    // setを宣言
    multiset<Book> s;

    // データ追加
    s.insert(Book("石橋",   "C言語入門") );
    s.insert(Book("石橋",   "アセンブラ入門") );
    s.insert(Book("石橋",   "C++入門") );
    s.insert(Book("中島",   "夜会") );
    s.insert(Book("つんく", "モーニング娘。のすべて") );

    cout << setiosflags( ios::left ) << flush;

    // すべてを表示
    multiset<Book>::iterator itr    = s.begin();
    multiset<Book>::iterator itrEnd = s.end();
    for( ; itr != itrEnd ; itr++ )
        cout << setw(20) << itr->Author << setw(20) << itr->Title << endl;

    cout << endl;

    // 著者が石橋のものを表示
    pair<multiset<Book>::iterator, multiset<Book>::iterator> p = s.equal_range((Book)"石橋");
    for( ; p.first != p.second ; p.first++ )
        cout << setw(20) << (p.first)->Author << setw(20) << (p.first)->Title << endl;
}

つんく              モーニング娘。のすべて
石橋                C言語入門
石橋                アセンブラ入門
石橋                C++入門
中島                夜会

石橋                C言語入門
石橋                アセンブラ入門
石橋                C++入門

    次は、multisetを使った、アンケート結果表示プログラムの例を示します。
// STLを使うとwarningが出る可能性があるので、それを出さないようにする
#pragma warning (disable:4786)

#include<set>
#include<iostream.h>
#include <iomanip.h>
#include"string.h"

using namespace std;

// アンケートの選択肢
enum response{
    no_answer,
    very_good,
    good,
    bad,
    very_bad
};

void main(){
    // multisetを宣言
    multiset<response>  s;

    // アンケート結果入力
    s.insert(no_answer);
    s.insert(very_good);
    s.insert(bad);
    s.insert(no_answer);
    s.insert(good);
    s.insert(bad);
    s.insert(very_good);
    s.insert(no_answer);
    s.insert(very_good);
    s.insert(very_bad);
    s.insert(good);

    // アンケート結果
    int result_no_answer = s.count(no_answer);
    int result_very_good = s.count(very_good);
    int result_good      = s.count(good);
    int result_bad       = s.count(bad);
    int result_very_bad  = s.count(very_bad);

    // 有効解答数
    int valid = s.size() - result_no_answer;

    // 結果表示
    cout << setiosflags( ios::left ) << flush;
    cout << "very_good : " << result_very_good << "  (" << 100 * result_very_good / valid << "%)" << endl;
    cout << "     good : " << result_good      << "  (" << 100 * result_good      / valid << "%)" << endl;
    cout << "      bad : " << result_bad       << "  (" << 100 * result_bad       / valid << "%)" << endl;
    cout << " very_bad : " << result_very_bad  << "  (" << 100 * result_very_bad  / valid << "%)" << endl;
    cout << endl;
    cout << "valid result : " << valid << endl;
}

very_good : 3  (37%)
     good : 2  (25%)
      bad : 2  (25%)
 very_bad : 1  (12%)

valid result : 8


7章：STL

17. bitset

    bitsetはすでに説明したように、true / falseを格納するのに適したコンテナです。 他のコンテナに「bool」型を格納した場合、bool型は1バイトの領域を必要とするので、 メモリー領域を無駄に使います。そこで、bitsetコンテナを使用します。

    bitsetにはイタレーターがありません。したがってアルゴリズムは使用できません。

bitsetクラスの主要な関数
関数名 | 書　式 | 説　明
set | bitset<N>& set()() | すべての要素を1(true)にし、自分自身を返します。
set bitset<N>& set(size_t i, int val) i番目の要素をvalにし、自分自身を返します。
valは0か1を指定します。
reset | bitset<N>& reset()() | すべての要素を0(false)にし、自分自身を返します。
reset | bitset<N>& reset(size_t i) | i番目の要素を0(false)にし、自分自身を返します。
flip | bitset<N>& flip()() | すべての要素を反転し、自分自身を返します。
flip | bitset<N>& flip(size_t i) | i番目の反転し、自分自身を返します。
size | size_t size() const()const | bitsetが保持できるビットの数を返します
count | size_type count() const()const | 1(true)である要素の数を返します。
test bool test(size_t i)const i番めの要素を返します。
iが無効な場合は例外(out_of_range)を投げます。
at bool at(size_t i)const i番めの要素を返します。
iが無効な場合は例外(out_of_range)を投げます。
any bool any()const すべての要素が0(false)なら0(false)を返します。
１つでも1(true)があれば1(true)を返します。
none bool none()const すべての要素が0(false)なら1(true)を返します。
１つでも1(true)があれば0(false)を返します。
to_string | string to_string()const | bitsetを文字列(ex:"0110100")で返します。
to_ulong unsigned long to_ulong()const bitsetを符号なしlong型として返します。
ただし、bitsetが32よりも多くの要素からなる場合は、例外(overflow_error)が投げられます。
operator== | bool operator==(bitset<N>& op2) | op2と同じであればtrueを返します。
operator!= | bool operator==(bitset<N>& op2) | op2と異なればtrueを返します。
operator&= | bitset<N>& operator&=(bitset<N>& op2) | op2との論理和を代入します。
operator^= | bitset<N>& operator^=(bitset<N>& op2) | op2との排他的論理和を代入します。
operator|= | bitset<N>& operator|=(bitset<N>& op2) | op2との論理積を代入します。
operator~ | bitset<N> operator~()const | 論理否定を返します。
operator<< bitset<N> operator<<(sizt_t num)const 符号なし整数として左にシフトものを返します。
つまり、一番右は0が入ります。
operator>> bitset<N> operator>>(sizt_t num)const 符号なし整数として右にシフトものを返します。
つまり、一番左は0が入ります。
operator<<= bitset<N>& operator<<=(sizt_t num) 符号なし整数として左にシフトし、自分自身を返します。
つまり、一番右は0が入ります。
operator>>= bitset<N>& operator>>=(sizt_t num) 符号なし整数として右にシフトし、自分自身を返します。
つまり、一番左は0が入ります。
operator[] reference& operator[](sizt_t i) i番目の要素の参照を返します。
iが無効な場合の動作は未定義です。

コンストラクタ
書　式 説　明
bitset()
例) bitset<48> bit1();
48bitsからなるbitset(要素はすべて0)を定義して、bit1とする。
bitset(unsigned long& ul)
例) bitset<32> bit2((unsigned long)0xFFFFFFFF);
例) bitset<48> bit3(unsigned long)0xFFFFFFFF);
すべて1の32bitsからなるbitsetを作成し、bit2とする
000000000000000011111111111111111111111111111111というbitsetを作り、bit3とする。
bitset(const string& s, size_t pos = 0, size_t n = -1)
例) bitset<8> bit4((string)"00110110"); 		strからbitsetを作ります。
00110110というbitsetを作り、bit4とする。

    なお、stringクラスについては後述します。

    次のページでは実際に使ってみます。


7章：STL

    ここではbitsetを使った例を示します。
// STLを使うとwarningが出る可能性があるので、それを出さないようにする
#pragma warning (disable:4786)

#include<bitset>
#include<iostream.h>
#include <iomanip.h>
#include<string>
using namespace std;

void main(){
    // 48bitsからなるbitsetを宣言
    bitset<48>    bit;

    // bitsetを表示するためにはこのように記述する
    // デフォルトコンストラクタではすべて0にする
    cout << bit.to_string().c_str() << endl;

    // 1, 2番目を1にする
    bit[1] = 1;
    bit.at(2) = true;

    // test関数は、値の取得のみなので、設定はできない
//  bit.test(3) = 1;

    // 表示
    cout << bit.to_string().c_str() << endl;

    // 範囲外は例外が投げられる
    try{
        bit.at(50) = 1;
    }catch(out_of_range e){
        cerr << "Exception : " << e.what() << endl;
    }

    // シフトしてみる
    bit <<= 10;
    cout << bit.to_string().c_str() << endl;

    // 反転してみる
    bit.flip();
    cout << bit.to_string().c_str() << endl;

    // unsigned longに変換する
    // ただし、大きすぎて変換できない
    try{
        unsigned long ulong = bit.to_ulong();
        cout << ulong << endl;
    }
    catch(overflow_error e){
        cerr << "Exception : " << e.what() << endl;
    }
}

000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000110
Exception : invalid bitset position
000000000000000000000000000000000001100000000000
111111111111111111111111111111111110011111111111
Exception : bitset conversion overflow

    次は、bitsetを使った、素数を求めるプログラム(エラトステネスのふるい)を紹介します。
// STLを使うとwarningが出る可能性があるので、それを出さないようにする
#pragma warning (disable:4786)

#include<bitset>
#include<iostream.h>
#include<iomanip.h>
#include<string>
#include<strstrea.h>

using namespace std;

#define  max 1000

void main(){
    // bitsetの宣言
    bitset<max+1>  bit;
    int i,j;

    // すべて1にする
    bit.set();

    // 0番め、1番め、2番目を0にする
    bit[0] = bit[1] = 0;

    // 素数を求める
    for( i = 2 ; i*i <= max ; i++ ){
        for( j = 2 ; i * j <= max && bit[i] != 0 ; j++ )
            bit[i*j] = 0;
    }

    // 表示
    for( i = 2 ; i <= max ; i++ ){
        if( bit[i] )
            cout << setw(10) << i << flush;
    }
    cout << endl;
}

         2         3         5         7        11        13        17        19
        23        29        31        37        41        43        47        53
        59        61        67        71        73        79        83        89
        97       101       103       107       109       113       127       131
       137       139       149       151       157       163       167       173
       179       181       191       193       197       199       211       223
       227       229       233       239       241       251       257       263
       269       271       277       281       283       293       307       311
       313       317       331       337       347       349       353       359
       367       373       379       383       389       397       401       409
       419       421       431       433       439       443       449       457
       461       463       467       479       487       491       499       503
       509       521       523       541       547       557       563       569
       571       577       587       593       599       601       607       613
       617       619       631       641       643       647       653       659
       661       673       677       683       691       701       709       719
       727       733       739       743       751       757       761       769
       773       787       797       809       811       821       823       827
       829       839       853       857       859       863       877       881
       883       887       907       911       919       929       937       941
       947       953       967       971       977       983       991       997


7章：STL

18. アルゴリズム

    各コンテナにはさまざまなメンバー関数が用意されています。それではなぜ別途アルゴリズム関数が 用意されているのでしょうか？

１つのアルゴリズムで複数のコンテナを扱える
    コンテナとアルゴリズムは直接的なつながりではなく、イタレーターを介しているために、 アルゴリズムはイタレーターを知っていればよく、コンテナ自体を知っている必要がない。 このため、アルゴリズムは複数のコンテナに適用できる。ただし、すでに解説したように イタレーターには５つの種類があるため、高機能なイタレーターをサポートしないコンテナには 適用できない場合もある。

STLの拡張性を高められる
    アルゴリズムはプログラマーが独自に作成することもできる。独自に作成したアルゴリズムは 既存のコンテナに適用することもできる。逆に、プログラマーが作成したコンテナに対して 既存のアルゴリズムを適用することもできる。つまり、STLの機能をプログラマーが拡張することが できる。

STLの合理化
    例えば、ソート機能や検索機能はよく使われる機能である。これを各コンテナクラスのメンバー関数と して実装すると、すべてのコンテナにソートや検索関数を実装しなければならない。これは非効率である。 複数のコンテナに対してよく使われる関数は、外部で作成した方が、効率が良い。

    ではアルゴリズムにはどのような関数が用意されているのでしょうか？ 大きく分けると以下のように分類できる。

  1. コピー操作、交換、マージ
  2. 要素の置換と削除
  3. ソートされていないシーケンスの検索
  4. ソート済みのシーケンスの検索
  5. ソート
  6. シーケンスの並び順の変更(ソートを除く)
  7. 最小値と最大値
  8. 集合演算
  9. シーケンスの変換・生成
  10. カウント
  11. ヒープ演算
  12. 順列

    次のページでは具体的な関数を紹介します。


7章：STL

18-2. コピー操作、交換、マージ

    コピー操作、交換、マージに分類される関数を紹介します。

    その前に、書式で使用する略称の紹介をします。

略称一覧
略　称 | 解　説
InIt | 入力イタレーター
OutIt | 出力イタレーター
FwdIt | 前方向イタレーター
BiIt | 双方向イタレーター
RndIt | ランダムアクセスイタレーター
T | データ型
Size | サイズ(整数型)
Func | 関数型
BinPred | 二項述語(引数を２つとり、trueかfalseを返す関数)
UniPred | 一項述語(引数を１つとり、trueかfalseを返す関数)

関数名 | 書　式 | 解　説
copy OutIt copy( InIt first, InIt last, OutIt x ) firstからlastの要素をxに追加(コピー)します。
戻り値は最後に追加した要素を指すイタレーターです。
copy_backward | BiIt copy_backward( BiIt first, BiIt last, BiIt x ) | 逆順にコピーを行う以外は、上述のcopyと同じです。
iter_swap | void iter_swap( FwdIt x, FwdIt y ) | xで示される要素とyで示される要素を交換します。
swap | void swap( T& x, T& y ) | firstxyを交換します。
swap_ranges FwdIt swap_ranges(FwdIt first, FwdIt last, FwdIt first2 ) firstからlastの要素をfirst2から始まる要素と交換します。
戻り値はfirst2で示されたシーケンスの末尾を表わすイタレーターです。
merge OutIt merge(InIt1 first1, InIt1 last2, InIt2 first2, InIt2 last2, OutIt result) first1からlast1の要素とfirst2からlast2の要素を連結し、resultに格納します。
戻り値は、結果のシーケンスの末尾です。
first1からlast1の要素とfirst2からlast2の要素がソートされていれば、結果もソートされています。
merge OutIt merge(InIt1 first1, InIt1 last2, InIt2 first2, InIt2 last2, OutIt result, Comp cmpfn) first1からlast1の要素とfirst2からlast2の要素を連結し、resultに格納します。
戻り値は、結果のシーケンスの末尾です。
first1からlast1の要素とfirst2からlast2の要素がソートされていれば、結果もソートされています。
ソートするための比較関数として、cmpfnが使われます。
inplace_merge void implace_merge(BiIt first, InIt mid, InIt last) firstからmidの要素とmidからlastの要素を連結し、firstに格納します。
同一コンテナ内でソートされている前半とソートされている後半を全体を通してソートしなおすときに用います。
inplace_merge void implace_merge(BiIt first, InIt mid, InIt last, Comp cmpfn) firstからmidの要素とmidからlastの要素を連結し、firstに格納します。
同一コンテナ内でソートされている前半とソートされている後半を全体を通してソートしなおすときに用います。
ソートするための比較関数として、cmpfnが使われます。


7章：STL

18-3. 要素の置換と削除

略称一覧
略　称 | 解　説
InIt | 入力イタレーター
OutIt | 出力イタレーター
FwdIt | 前方向イタレーター
BiIt | 双方向イタレーター
RndIt | ランダムアクセスイタレーター
T | データ型
Size | サイズ(整数型)
Func | 関数型
BinPred | 二項述語(引数を２つとり、trueかfalseを返す関数)
UniPred | 一項述語(引数を１つとり、trueかfalseを返す関数)

関数名 | 書　式 | 解　説
remove FwdIt remove( FwdIt first, FwdIt last, const T& val ) firstからlastのシーケンスからvalと等しい要素を削除します。
戻り値は、末尾を表わすイタレーターです。
remove_if FwdIt remove_if( FwdIt first, FwdIt last, UniPred pr ) prで評価され、trueが返る要素を削除します。
戻り値は、末尾を表わすイタレーターです。
remove_copy OutIt remove_copy( InIt first, InIt last, OutIt x, const T& val ) firstからlastのシーケンスからvalと等しい要素を削除し、xにコピーします。
結果の末尾を表わすイタレーターを返します。
remove_copy_if OutIt remove_copy_if( InIt first, InIt last, OutIt x, UniPred pr ) prで評価され、trueが返る要素を削除し、xにコピーします。
結果の末尾を表わすイタレーターを返します。
replace | void replace( FwdIt first, FwdIt last, const T& vold, const T& vnew ) | firstからlastにvoldと等しい要素があれば、それをvnewに置き換えます。
replace_if | void replace_if( FwdIt first, FwdIt last, UniPred pr, const T& vnew ) | prで評価され、trueが返る要素を、vnewに置換します。
replace_copy | OutIt replace_copy( InIt first, InIt last, OutIt x, const T& vold, const T& vnew ) | firstからlastにvoldと等しい要素があれば、それをvnewに置き換え、xにコピーします。
replace_copy_if | OutIt replace_copy_if( InIt first, InIt last, OutIt x, UniPred pr, cont T& vnew ) | prで評価され、trueが返る要素をvnewで置換し、古い要素をxにコピーします。
unique FwdIt unique( FwdIt first, FwdIt last ) 連続した重複要素を削除します。
戻り値は指定範囲の末尾を返します。
unique FwdIt unique( FwdIt first, FwdIt last, BinPred pr ) 等値かどうかの判断をprを用いて調べる以外は上記と同じです。
戻り値は指定範囲の末尾を返します。
unique_copy OutIt unique_copy( InIt first, InIt last, OutIt x ) 連続した重複要素をxにコピーしながら削除します。
戻り値は指定範囲の末尾を返します。
unique_copy OutIt unique_copy( InIt first, InIt last, OutIt x, BinPred pr ) 等値かどうかの判断をprを用いて調べる以外は上記と同じです。
戻り値は指定範囲の末尾を返します。
fill | void fill( FwdIt first, FwdIt last, const T& x ) | firstからlastの要素をxで満たします。
fill_n | void fill( FwdIt first, Size n, const T& x ) | firstからn個分の要素をxで満たします。


7章：STL

18-4. ソートされていないシーケンスの検索

略称一覧
略　称 | 解　説
InIt | 入力イタレーター
OutIt | 出力イタレーター
FwdIt | 前方向イタレーター
BiIt | 双方向イタレーター
RndIt | ランダムアクセスイタレーター
T | データ型
Size | サイズ(整数型)
Func | 関数型
BinPred | 二項述語(引数を２つとり、trueかfalseを返す関数)
UniPred | 一項述語(引数を１つとり、trueかfalseを返す関数)

関数名 | 書　式 | 解　説
adjacent_find | FwdIt adjacent_find(FwdIt first, FwdIt last) | firstからlastのシーケンスから連続した同位置の要素を見つけ、その最初の要素を指すイタレーターを返します。
adjacent_find FwdIt adjacent_find(FwdIt first, FwdIt last, BinPred pr) firstからlastのシーケンスから連続した同位置の要素を見つけ、その最初の要素を指すイタレーターを返します。
同一かどうかを調べるのにpr関数を使用します。
equal | bool equal(InIt1 first1, InIt1 last1, InIt2 first2) | first1からlast1の要素が、first2から始まる要素と同じ場合はtrueが帰ります。
equal bool equal(InIt1 first1, InIt1 last1, InIt2 fist2, BinPred pr) first1からlast1の要素が、first2から始まる要素と同じ場合はtrueが帰ります。
等値かどうかの判断にprを用います。
find | InIt remove_copy(InIt first, InIt last, const T& val) | firstからlastのシーケンスからvalと等しい要素を検索し、最初に見つけた要素を指すイタレーターを返します。
find_end | FwdIt1 find_end(FwdIt1 first1, FwdIt1 last1, FwdIt1 first2, FwdIt1 last2) | first1からlast1のシーケンスからfirst2からlast2の部分シーケンスを検索します。見つかれば見つかった部分シーケンスの末尾を指すイタレーターを返します。見つからない場合はlast1を返します。
find_end | FwdIt1 find_end(FwdIt1 first1, FwdIt1 last1, FwdIt1 first2, FwdIt1 last2, BinPred pr) | first1からlast1のシーケンスからfirst2からlast2の部分シーケンスを検索します。見つかれば見つかった部分シーケンスの末尾を指すイタレーターを返します。見つからない場合はlast1を返します。等しいかどうかの判断にprを用います。
find_if | InIt find_if( InIt first, InIt last, UniPred pr) | firstからlastからprがtrueを返すような要素を検索し、最初に見つかった要素を指すイタレーターを返します。
find_first_of | FwdIt1 replace_if(FwdIt first1, FwdIt last1, FwdIt2 first2, FwdIt2 last2) | first1からlast1の要素にfirst2からlast2の要素のいずれかが含まれれば、その要素を指すイタレーターを返します。含まれていなければ、last1を返します。
find_first_of | FwdIt1 replace_if(FwdIt first1, FwdIt last1, FwdIt2 first2, FwdIt2 last2, BinPred pr) | first1からlast1の要素にfirst2からlast2の要素のいずれかが含まれれば、その要素を指すイタレーターを返します。含まれていなければ、last1を返します。等値かどうかの判断にprを使用します。
mismatch | pair<InIt1, InIt2> mismatch(InIt1 first, InIt1 last, InIt2 first2) | firstからlastからのシーケンスと、first2からのシーケンスを比較し、初めて異なる部分のイタレーターのペアを返します。等しければ、各シーケンスの末尾を指すイタレーターのペアを返します。
mismatch | pair<InIt1, InIt2> mismatch(InIt1 first, InIt1 last, InIt2 first2, BinPred pr) | firstからlastからのシーケンスと、first2からのシーケンスを比較し、初めて異なる部分のイタレーターのペアを返します。等しければ、各シーケンスの末尾を指すイタレーターのペアを返します。等値かどうかの判断にprを使用します。
search FwdIt search( FwdIt first1, FwdIt last1, FwdIt first2, FwdIt last2 ) first2からlast2の部分シーケンスをfist1からlast1から検索します。
戻り値は検索の結果、見つかったシーケンスの先頭のイタレーターです。
見つからなかった場合はlast2が返されます。
search | FwdIt search( FwdIt first1, FwdIt last1, FwdIt first2, FwdIt last2, Pred pr ) | 等しいかどうかの二項述語を指定する以外は上記と同じです。
search_n FwdIt search_n( FwdIt first, FwdIt last, Size n, const T& val ) firstからlastのシーケンスからvalがn個連続する部分シーケンスを検索します。
戻り値は検索の結果、見つかったシーケンスの先頭のイタレーターです。
見つからなかった場合はlastが返されます。
search_n | FwdIt search_n( FwdIt first, FwdIt last, Size n, const T& val, Pred pr ) | 等しいかどうかの二項述語を指定する以外は上記と同じです。


7章：STL

18-5. ソート済みのシーケンスの検索

略称一覧
略　称 | 解　説
InIt | 入力イタレーター
OutIt | 出力イタレーター
FwdIt | 前方向イタレーター
BiIt | 双方向イタレーター
RndIt | ランダムアクセスイタレーター
T | データ型
Size | サイズ(整数型)
Func | 関数型
BinPred | 二項述語(引数を２つとり、trueかfalseを返す関数)
UniPred | 一項述語(引数を１つとり、trueかfalseを返す関数)

関数名 | 書　式 | 解　説
binary_search | bool binary_search(FwdIt first, FwdIt last, const T& val) | firstからlastのシーケンス内にvalが存在すればtrueを返す。
binary_search | bool binary_search(FwdIt first, FwdIt last, const T& val, BinPred pr) | firstからlastのシーケンス内にvalが存在すればtrueを返す。operator<の代わりにprを用いて比較します。
equal_range | pair<FwdIt, FwdIt> equal_range(FwdIt first, FwdIt last, const T& val) | pair(lower_bound(first, last, val), upper_bound(first, last, val))を返します。言い換えると、firstからlastの範囲からvalと等しい範囲のイタレーターのペアを返します。
equal_range | pair<FwdIt, FwdIt> equal_range(FwdIt first, FwdIt last, const T& val, BinPred pr) | pair(lower_bound(first, last, val, pr), upper_bound(first, last, val, pr))を返します。
lower_bound | FwdIt lower_bound(InIt first, InIt last, const T& val) | firstからlastのシーケンスからval以上の最初の要素を指すイタレーターを返します。ない場合には、lastを返します。
lower_bound | FwdIt lower_bound(InIt first, InIt last, const T& val, BinPred pr) | firstからlastのシーケンスからval以上の最初の要素を指すイタレーターを返します。ない場合には、lastを返します。operator<の代わりにprを用いて比較します。
upper_bound | FwdIt upper_bound(InIt first, InIt last, const T& val) | firstからlastのシーケンスからval以下の最後の要素を指すイタレーターを返します。ない場合には、lastを返します。
upper_bound | FwdIt upper_bound(InIt first, InIt last, const T& val, BinPred pr) | firstからlastのシーケンスからval以下の最後の要素を指すイタレーターを返します。ない場合には、lastを返します。operator<の代わりにprを用いて比較します。


7章：STL

18-6. ソート

略称一覧
略　称 | 解　説
InIt | 入力イタレーター
OutIt | 出力イタレーター
FwdIt | 前方向イタレーター
BiIt | 双方向イタレーター
RndIt | ランダムアクセスイタレーター
T | データ型
Size | サイズ(整数型)
Func | 関数型
BinPred | 二項述語(引数を２つとり、trueかfalseを返す関数)
UniPred | 一項述語(引数を１つとり、trueかfalseを返す関数)

関数名 | 書　式 | 解　説
sort | void sort(RanIt first, RanIt last) | firstからlastのシーケンスを昇順にソートします。
sort | void sort(RanIt first, RanIt last, BinPred pr) | firstからlastのシーケンスをソートします。operator<の代わりにprを用いて比較します。
stable_sort | void stable_sort(RanIt first, RanIt last) | firstからlastのシーケンスを昇順にソートします。等しい要素の並び順は維持されます。
stable_sort | void stable_sort(RanIt first, RanIt last, BinPred pr) | firstからlastのシーケンスをソートします。等しい要素の並び順は維持されます。operator<の代わりにprを用いて比較します。
partial_sort | void partial_sort(RanIt first, RanIt mid, RanIt last) | firstからlastのシーケンスを昇順にソートします。ただし実行後ソートされている範囲はstartからmidまでです。
partial_sort | void partial_sort(RanIt first, RanIt mid, RanIt last, BinPred pr) | firstからlastのシーケンスをソートします。ただし実行後ソートされている範囲はstartからmidまでです。operator<の代わりにprを用いて比較します。
partial_sort_copy | RanIt partial_sort_copy(InIt first1, InIt last1, RanIt first2, RanIt last2) | first1からlast1のシーケンスをソートし、その結果を可能な限りfirst2からlast2にコピーします。戻り値はコピーした範囲の末尾を指すイタレーターです。
partial_sort_copy | RanIt partial_sort_copy(InIt first1, InIt last1, RanIt first2, RanIt last2, BinPred pr) | first1からlast1のシーケンスをソートし、その結果を可能な限りfirst2からlast2にコピーします。戻り値はコピーした範囲の末尾を指すイタレーターです。operator<の代わりにprを用いて比較します。
nth_element | void lower_bound(RanIt first, RanIt nth, RanIt last) | firstからlastのシーケンスをnthより小さい部分が大きい部分より前に来るように並べ替えます。クイックソートの一部分です。
nth_element | void lower_bound(RanIt first, RanIt nth, RanIt last, BinPred pr) | firstからlastのシーケンスをnthより小さい部分が大きい部分より前に来るように並べ替えます。operator<の代わりにprを用いて比較します。


7章：STL

18-7. シーケンスの並び順の変更(ソートを除く)

略称一覧
略　称 | 解　説
InIt | 入力イタレーター
OutIt | 出力イタレーター
FwdIt | 前方向イタレーター
BiIt | 双方向イタレーター
RndIt | ランダムアクセスイタレーター
T | データ型
Size | サイズ(整数型)
Func | 関数型
BinPred | 二項述語(引数を２つとり、trueかfalseを返す関数)
UniPred | 一項述語(引数を１つとり、trueかfalseを返す関数)

関数名 | 書　式 | 解　説
rotate | void rotate(FwdIt first, FwdIt middle, FwdIt last) | firstからlastのシーケンスを左方向に回転させ、middleが先頭に来るようにします。
rotate_copy | OutIt rotate_copy(FwdIt first, FwdIt middle, FwdIt last, OutIt x) | firstからlastのシーケンスを左方向に回転させ、middleが先頭に来るようにし、結果をxにコピーします。戻り値はコピーしたシーケンスの末尾を指すイタレーターです。
random_shuffle | void random_shuffle(RanIt first, RanIt last) | firstからlastのシーケンスをランダムに入れ換えます。ただし、ランダム関数はseedを設定していません。
random_shuffle | void random_shuffle(RanIt first, RanIt last, Fun& f) | firstからlastのシーケンスをランダムに入れ換えます。乱数を出力する関数fを使用します。このfは「int Rand(int n)」の書式で、0からnまでの乱数を返す関数です。
reverse | void reverse(BidIt first, BidIt last) | firstからlastのシーケンスを逆順に並べ替えます。
reverse_copy | OutIt reverse_copy(BidIt first, BidIt last, OutIt x) | firstからlastのシーケンスを逆順に並び替え、結果をxに出力します。戻り値は、コピーされたシーケンスの末尾を指すイタレーターです。
partition | BidIt partition(BidIt first, BidIt last, UniPred pr) | firstからlastのシーケンスをprがtrueを返す要素が、falseを返す要素より前に来るように並び替えます。戻り値はfalseが返る要素の最初を指すイタレーターです。
stable_partition | FwdIt stable_partition(FwdIt first, FwdIt last, UniPred pr) | firstからlastのシーケンスをprがtrueを返す要素が、falseを返す要素より前に来るように並び替えます。ただし、等しい要素の並び順は維持されます。戻り値はfalseが返る要素の最初を指すイタレーターです。


7章：STL

18-8. 最小値と最大値

略称一覧
略　称 | 解　説
InIt | 入力イタレーター
OutIt | 出力イタレーター
FwdIt | 前方向イタレーター
BiIt | 双方向イタレーター
RndIt | ランダムアクセスイタレーター
T | データ型
Size | サイズ(整数型)
Func | 関数型
BinPred | 二項述語(引数を２つとり、trueかfalseを返す関数)
UniPred | 一項述語(引数を１つとり、trueかfalseを返す関数)

関数名 | 書　式 | 解　説
max | const T& max(const T& x, const T& y) | xとyの大きいほうを返します。
max | const T& max(const T& x, const T& y, BinPred pr) | xとyの大きいほうを返します。operator<の代わりにprを使用します。
max_element | FwdIt max_element(FwdIt first, FwdIt last) | firstからlastのシーケンスから最大の要素を指すイタレーターを返します。
max_element | FwdIt max_element(FwdIt first, FwdIt last, BinPred pr) | firstからlastのシーケンスから最大の要素を指すイタレーターを返します。operator<の代わりにprを使用します。
min | const T& min(const T& x, const T& y) | xとyの小さいを返します。
min | const T& min(const T& x, const T& y, BinPred pr) | xとyの小さいほうを返します。operator<の代わりにprを使用します。
min_element | FwdIt min_element(FwdIt first, FwdIt last) | firstからlastのシーケンスから最少の要素を指すイタレーターを返します。
min_element | FwdIt min_element(FwdIt first, FwdIt last, BinPred pr) | firstからlastのシーケンスから最少の要素を指すイタレーターを返します。operator<の代わりにprを使用します。


7章：STL

18-9. 集合演算

略称一覧
略　称 | 解　説
InIt | 入力イタレーター
OutIt | 出力イタレーター
FwdIt | 前方向イタレーター
BiIt | 双方向イタレーター
RndIt | ランダムアクセスイタレーター
T | データ型
Size | サイズ(整数型)
Func | 関数型
BinPred | 二項述語(引数を２つとり、trueかfalseを返す関数)
UniPred | 一項述語(引数を１つとり、trueかfalseを返す関数)

関数名 | 書　式 | 解　説
includes | bool includes(InIt1 first1, InIt1 last1, InIt2 first2, InIt2 last2) | first1からlast1の要素に、first2からlast2までの要素のすべてが含まれていればtrueを返します。
includes | bool includes(InIt1 first1, InIt1 last1, InIt2 first2, InIt2 last2, BinPred pr) | first1からlast1の要素に、first2からlast2までの要素のすべてが含まれていればtrueを返します。operator==の代わりにprを用いて比較します。
set_difference | OutIt set_difference(InIt1 first1, InIt1 last1, InIt2 first2, InIt2 last2, OutIt x) | ソート済みのfirst1からlast1にあって、ソート済みのfirst2からlast2にないものを、xからのシーケンスにコピーしていきます。コピー後の末尾を指すイタレーターを返します。
set_difference | OutIt set_difference(InIt1 first1, InIt1 last1, InIt2 first2, InIt2 last2, OutIt x, BinPred pr) | ソート済みのfirst1からlast1にあって、ソート済みのfirst2からlast2にないものを、xからのシーケンスにコピーしていきます。コピー後の末尾を指すイタレーターを返します。operator==の代わりにprを用いて比較します。
set_symmetric_difference | OutIt set_symmetric_difference(InIt1 first1, InIt1 last1, InIt2 first2, InIt2 last2, OutIt x) | ソート済みのfirst1からlast1と、ソート済みのfirst2からlast2のうち、どちらか一方に含まれるものをxからのシーケンスにコピーしていきます。コピー後の末尾を指すイタレーターを返します。
set_symmetric_difference | OutIt set_symmetric_difference(InIt1 first1, InIt1 last1, InIt2 first2, InIt2 last2, OutIt x, BinPred pr) | ソート済みのfirst1からlast1と、ソート済みのfirst2からlast2のうち、どちらか一方に含まれるものをxからのシーケンスにコピーしていきます。コピー後の末尾を指すイタレーターを返します。operator==の代わりにprを用いて比較します。
set_intersection | OutIt set_intersection(InIt1 first1, InIt1 last1, InIt2 first2, InIt2 last2, OutIt x) | ソート済みのfirst1からlast1と、ソート済みのfirst2からlast2のうち、両方に存在する要素をxからのシーケンスにコピーしていきます。コピー後の末尾を指すイタレーターを返します。
set_intersection | OutIt set_intersection(InIt1 first1, InIt1 last1, InIt2 first2, InIt2 last2, OutIt x, BinPred pr) | ソート済みのfirst1からlast1と、ソート済みのfirst2からlast2のうち、両方に存在する要素をxからのシーケンスにコピーしていきます。コピー後の末尾を指すイタレーターを返します。operator==の代わりにprを用いて比較します。
set_union | OutIt set_union(InIt1 first1, InIt1 last1, InIt2 first2, InIt2 last2, OutIt x) | ソート済みのfirst1からlast1と、ソート済みのfirst2からlast2のうち、どちらかまたは両方に含まれる要素をxからのシーケンスにコピーしていきます。コピー後の末尾を指すイタレーターを返します。
set_union | OutIt set_union(InIt1 first1, InIt1 last1, InIt2 first2, InIt2 last2, OutIt x, BinPred pr) | ソート済みのfirst1からlast1と、ソート済みのfirst2からlast2のうち、どちらかまたは両方に含まれる要素をxからのシーケンスにコピーしていきます。コピー後の末尾を指すイタレーターを返します。operator==の代わりにprを用いて比較します。


7章：STL

18-10. シーケンスの変換・生成

略称一覧
略　称 | 解　説
InIt | 入力イタレーター
OutIt | 出力イタレーター
FwdIt | 前方向イタレーター
BiIt | 双方向イタレーター
RndIt | ランダムアクセスイタレーター
T | データ型
Size | サイズ(整数型)
Func | 関数型
BinPred | 二項述語(引数を２つとり、trueかfalseを返す関数)
UniPred | 一項述語(引数を１つとり、trueかfalseを返す関数)

関数名 | 書　式 | 解　説
for_each | Func for_each(InIt first, InIt last, Func f) | firstからlastの各要素を引数にして、関数fを呼び出します。
transform | OutIt transform(InIt first, InIt last, OutIt x, UniPred pr) | firstからlastの各要素に、単項演算子prを呼び出し、その結果をxからのシーケンスに代入していきます。戻り値は代入したシーケンスの末端を指すイタレーターです。
transform | OutIt transform(InIt1 first1, InIt1 last1, InIt1 first1, OutIt x, BinPred pr) | first1からlast1の各要素に、２項演算子pr(引数はfirst2からはじまる要素)を呼び出し、その結果をxからのシーケンスに代入していきます。戻り値は代入したシーケンスの末端を指すイタレーターです。
generate | void generate(FwdIt first, FwdIt last, Gen g) | 生成関数g()を実行し、その関数の戻り値をfirstからlastの各要素にコピーします。
generate_n | void generate_n(OutIt first1, Dist n, Gen g) | 生成関数g()を実行し、その関数の戻り値をfirstからn個分の各要素にコピーします。


7章：STL

18-11. カウント

略称一覧
略　称 | 解　説
InIt | 入力イタレーター
OutIt | 出力イタレーター
FwdIt | 前方向イタレーター
BiIt | 双方向イタレーター
RndIt | ランダムアクセスイタレーター
T | データ型
Size | サイズ(整数型)
Func | 関数型
BinPred | 二項述語(引数を２つとり、trueかfalseを返す関数)
UniPred | 一項述語(引数を１つとり、trueかfalseを返す関数)

関数名 | 書　式 | 解　説
count | size_t count(InIt first, InIt last, const T& val) | firstからlastのシーケンスの中に、valと等しいものがいくつあるのかを数えます。
count_if | size_t count_if(InIt first, InIt last, UniPred pr) | firstからlastの各要素を引数に、関数prを呼び出し、その戻り値がtrueの数を数えます。


7章：STL

19. basic_string

    basic_stringとは、文字列を表わすコンテナです。basic_stringには「string」 クラスと、「wstring」クラスがあります。前者のstringクラスでは、今まで 使用してきたような文字列を管理し、後者のwstringクラスでは、ワイド文字をサポートする文字列を 扱います。なお、本ホームページではstringクラスのみ使用します。

    basic_stringは、STLで言うコンテナです。文字列を扱うクラスのように思われるかもしれませんが、 文字を要素とするコンテナと考えれば、コンテナクラスに属することも納得できると思います。

    なお、basic_stringは、本当の意味でのSTLの一部ではありません。しかし、STLと完全に互換性があること、 C++のほとんどのコンパイラがサポートしていること、便利なことなどから本ページではSTLの一部として 扱うこととします。


7章：STL

     コンストラクタ
書　式 説　明
string(const A& al = A())
例) string str;
からのstringを作る
string(size_type n, const char ch, const A& al = A())
例) string str2(20, 'A')
長さ20のstringを作り、'A'で埋める。
string(const char* str, const A& al = A() )
例) string str3("ABCDEF");
例) string str33 = "ABCDEF"
"ABCDEF"というstr3を作る。
"ABCDEF"というstr33を作る。
string(const char* str, size_type n, const A& al = A())
例) string str4("ABC", 2);
"ABC"という文字列の最初から2文字の文字列("AB")を作る。
string(const string str)
例) string str5(str3);
str3と同じ文字列を作る。
string(const string str, size_type pos, size_type n, const A& al = A() )
例) string str6(str3, 2, 3);
str3の2文字目から3文字の文字列("CDE")を作る。
string(const_iterator first, const_iterator last, const A& al = A() )
例) string str7(str3.begin(), str3.end() );
str3の最初から最後までの文字列を作る。
    これらstring(basic_string)型には、検索、追加、比較など多くのメンバー関数が用意されている。 ただし、今まで使用してきたchar*で'\0'で終わるという文字列(C型文字列、ASCIIZ文字列と呼ばれる)とは 互換性がない。つまり、C型文字列に変換するためには、そのためのメンバー関数を使用する必要がある。

#include <iostream>

// stringを使えるようにする
#include<string>
using namespace std;

void main(){
    string str = "12345";

    int i = atoi( str );              // ←コンパイルエラー
    int j = atoi( str.c_str() );      // ←正常に動作

    cout << str << endl;              // ←正常に動作
    printf("%s\n", str );             // ←実行時にエラー
    printf("%s\n", str.c_str() );     // ←正常に動作

    return;
}


7章：STL

    stringクラスにはさまざまなメンバー関数が用意されています。たくさんありますが、 簡単に言いますと、イタレーター関係、文字数を調べる関数、文字列や文字の代入、追加、 挿入を行う関数、比較関数、置換や検索を行う関数などがあります。

stringクラスの主要な関数
関数名 | 書　式 | 説　明
begin | iterator begin()() | １文字目を指すイタレーターを返します。
end | iterator end() | 末尾を指すイタレーターを返します。
rbegin | reverse_iterator rbegin()() | 末端を指す逆方向イタレーター(逆シーケンスの先頭を指すイタレーター)を返します。
rend | reverse_iterator rend(>) | 先頭を指す逆方向イタレーター(逆シーケンスの末尾を指すイタレーター)を返します。
size | size_type size()const | 文字数を返します。(lengthと同じです)※
length | size_type length()const | 文字数を返します。(sizeと同じです)※
empty | bool empty()const | バッファーサイズが0の時にtrueを返します。(文字数ではありません。)
at | reference at(size_type pos) | pos番目の文字の参照を返します。posが無効な場合は例外(out_of_range)を投げます。
append | string& append(const char* s) | NULLで終わる文字列sを追加し、自分自身の参照を返します。
append | string& append(const char* s, size_type n) | NULLで終わる文字列sの最初からn文字分を追加し、自分自身の参照を返します。
append | string& append(const string& str) | strを追加し、自分自身の参照を返します。
append | string& append(const string& str, size_type pos, size_type n) | strのpos文字目からn文字分を追加し、自分自身の参照を返します。
append | string& append(size_type n, char c) | １文字cをn個追加し、自分自身の参照を返します。
append | string& append(const_iterator first, const_iterator last) | firstからlastの文字を追加し、自分自身の参照を返します。
insert | basic_string& insert(size_t pos, const char* s) | pos番めの文字の前に、NULLで終わる文字列sを挿入し、自分自身を返します。
insert | basic_string& insert(size_t pos, const char* s, syze_type n) | pos番めの文字の前に、sの最初からn文字分を挿入し、自分自身を返します。
insert | basic_string& insert(size_t pos, const basic& str) | pos番めの文字の前に、strを挿入し、自分自身を返します。
insert | basic_string& insert(size_t pos, const basic& str, size_t n) | pos番めの文字の前に、strの最初からn文字分を挿入し、自分自身を返します。
insert | basic_string& insert(size_t pos, size_t n, char c) | pos番めの文字の前に、文字cをn個分挿入し、自分自身を返します。
insert | iterator insert(iterator itr, char c) | itrが指す文字の前に、文字cを挿入し、挿入した文字を指すイタレーターを返します。
insert | void insert(iterator itr, size_type n, char c) | itrが指す文字の前に、文字cをn個分挿入します。
insert | void insert(iterator itr, const_iterator first, const_iterator last) | itrが指す文字の前に、firstからlastで表される文字列を挿入します。
erase | iterator erase(iterator& itr) | itrが指す文字を削除します。戻り値は削除した部分の次の文字を指すイタレーターを返します。
erase | iterator erase(iterator& first, iterator& last) | firstからlastまでの文字列を削除します。戻り値は削除した部分の次の文字を指すイタレーターを返します。
erase | string& erase(size_type& pos = 0, size_type& n = npos) | pos文字目からn文字分の部分文字列を削除し、自分自身を返します。
assign | basic_string& assign(const char* s) | NULLで終わる文字列sを自分自身に代入し、自分自身を返します。
assign | basic_string& assign(const char* s, size_type n) | NULLで終わる文字列sの最初からn文字分を自分自身に代入し、自分自身を返します。
assign | basic_string& assign(const string& str) | strを自分自身に代入し、自分自身を返します。
assign | basic_string& assign(const string& str, size_type pos, size_type n) | strのpos文字目からn文字分の文字列を自分自身に代入し、自分自身を返します。
assign | basic_string& assign(size_type n, char c) | 文字cをn個分を自分自身に代入し、自分自身を返します。
assign | basic_string& assign(const_iterator first, const_iterator last) | firstからlastで表される文字列を自分自身に代入し、自分自身を返します。
swap | void swap(string& str) | 自分自身と、strの内容を交換します。
substr | string substr(size_tyoe pos = 0, size_type n = npos)const | pos文字目からn文字分の部分文字列を作り、それを返します。
replace | string& replace(size_type pos, size_type n, const char* s) | pos文字目からn文字分の部分文字列を、NULLで終わる文字列sに置換します。
replace | string& replace(size_type pos, size_type n1, const char* s, size_type n2) | pos文字目からn1文字分の部分文字列を、NULLで終わる文字列sの最初からn2文字分の部分文字列に置換します。
replace | string& replace(size_type pos, size_type n, const string& str) | pos文字目からn文字分の部分文字列を、文字列strに置換します。
replace | string& replace(size_type pos1, size_type n1, const string& str, size_type pos2, size_type n2) | pos1文字目からn1文字分の部分文字列を、文字列strのpos文字目からn2文字分の部分文字列に置換します。
replace | string& replace(size_type pos1, size_type n1, size_type n2, char c) | pos1文字目からn1文字分の部分文字列を、文字cがn1個から形成される文字列に置換します。
replace | string& replace(iterator first, iterator last, const char* s) | firstからlastの部分文字列を、NULLで終わる文字列sに置換します。
replace | string& replace(iterator first, iterator last, const char* s, size_type n) | firstからlastの部分文字列を、NULLで終わる文字列sの最初からn文字分の部分文字列に置換します。
replace | string& replace(iterator first, iterator last, const string& str) | firstからlastの部分文字列を、文字列strに置換します。
replace | string& replace(iterator first, iterator last, size_type n, char c) | firstからlastの部分文字列を、文字cがn個から形成される文字列に置換します。
replace | string& replace(iterator first, iterator last, const_iterator first2, const_iterator last2) | first1からlast1の部分文字列を、first2からlast2の部分文字列に置換します。
compare int compare(const basic_string& str)const strと辞書順で比較し、結果を返します。
strcmp(this->c_str(), str.c_str())とほぼ同様です。
compare | int compare(size_type pos, size_type n, const string& str)const | pos文字目からn文字分の部分文字列とstrを辞書順で比較し、結果を返します。
compare | int compare(size_type pos1, size_type n1, const string& str, size_type pos2, size_type n2)const | pos1文字目からn1文字分の部分文字列とstrのpos2文字目からn2文字分の部分文字列を辞書順で比較し、結果を返します。
compare int compare(const char* s)const NULLで終わる文字列sと辞書順で比較し、結果を返します。
strcmp(this->c_str(), s)とほぼ同様です。
compare | int compare(size_type pos, size_type n, const char* s)const | pos文字目から、n文字分の部分文字列と、NULLで終わる文字列sを辞書順で比較し、結果を返します。
compare | int compare(size_type pos1, size_type n1, const char* s, size_type pos2)const | pos1文字目から、n1文字分の部分文字列と、NULLで終わる文字列sのpos2文字目から始まる部分文字列を辞書順で比較し、結果を返します。
find | size_type find(char c, size_type pos = 0)const | pos文字目から、文字cを検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find | size_type find(const char* s, size_type pos = 0)const | pos文字目から、文字列sを検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find | size_type find(const char* s, size_type pos, size_type n)const | pos文字目から、文字列sのn文字目までの部分文字列を検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find | size_type find(const string& str, size_type pos = 0)const | pos文字目から、文字列strを検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_first_of | size_type find_first_of(char c, size_type pos = 0)const | pos文字目から、文字cを検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_first_of | size_type find_first_of(const char* s, size_type pos = 0)const | pos文字目から、文字列s内に含まれる文字を検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_first_of | size_type find_first_of(const char* s, size_type pos, size_type n)const | pos文字目から、文字列sのn文字目までの部分文字列内に含まれる文字を検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_first_of | size_type find_first_of(const string& str, size_type pos = 0)const | pos文字目から、文字列str内に含まれる文字を検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_first_not_of | size_type find_first_not_of(char c, size_type pos = 0)const | pos文字目から、文字cではない文字を検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_first_not_of | size_type find_first_not_of(const char* s, size_type pos = 0)const | pos文字目から、文字列s内に含まれない文字を検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_first_not_of | size_type find_first_not_of(const char* s, size_type pos, size_type n)const | pos文字目から、文字列sのn文字目までの部分文字列内に含まれない文字を検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_first_not_of | size_type find_first_not_of(const string& str, size_type pos = 0)const | pos文字目から、文字列str内に含まれない文字を検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
rfind | size_type rfind(char c, size_type pos = npos)const | pos文字目から、文字cを逆順に検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
rfind | size_type rfind(const char* s, size_type pos = npos)const | pos文字目から、文字列sを逆順に検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
rfind | size_type rfind(const char* s, size_type pos, size_type n = npos)const | pos文字目から、文字列sのn文字目までの部分文字列を逆順に検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
rfind | size_type rfind(const string& str, size_type pos = npos)const | pos文字目から、文字列strを逆順に検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_last_of | size_type find_last_of(char c, size_type pos = npos)const | pos文字目から、文字cを逆順に検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_last_of | size_type find_last_of(const char* s, size_type pos = npos)const | pos文字目から、文字列s内に含まれる文字を逆順に検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_last_of | size_type find_last_of(const char* s, size_type pos, size_type n = npos)const | pos文字目から、文字列sのn文字目までの部分文字列内に含まれる文字を逆順に検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_last_of | size_type find_last_of(const string& str, size_type pos = npos)const | pos文字目から、文字列str内に含まれる文字を逆順に検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_last_not_of | size_type find_last_not_of(char c, size_type pos = npos)const | pos文字目から、文字cではない文字を逆順に検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_last_not_of | size_type find_last_not_of(const char* s, size_type pos = npos)const | pos文字目から、文字列s内に含まれない文字を逆順に検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_last_not_of | size_type find_last_not_of(const char* s, size_type pos, size_type n = npos)const | pos文字目から、文字列sのn文字目までの部分文字列内に含まれない文字を逆順に検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
find_last_not_of | size_type find_last_not_of(const string& str, size_type pos = npos)const | pos文字目から、文字列str内に含まれない文字を逆順に検索し、最初に見つかった文字のインデックスを返します。見つからない場合はメンバー変数nposを返します。
c_str | const char* c_str()const | コンテナの最後がNULL文字でなければ、NULL文字を付加して文字列の最初の文字を表すポインタを返します。C言語で定義される文字列へのキャストに使います。
data | const char* data()const | 文字列の最初の文字を表すポインタを返します。
max_size | size_type max_size()const | 生成できる文字列の最大文字数を返します。
operator= | basic_string& operator=(char& c) | 自分自身に文字cを代入します。
operator= | basic_string& operator=(const char* s) | 自分自身に文字列sを代入します。
operator= | basic_string& operator=(const string& str) | 自分自身に文字列strを代入します。
operator+= | basic_string& operator+=(char& c) | 自分自身の最後に文字cを追加します。
operator+= | basic_string& operator+=(const char* s) | 自分自身に最後に文字列sを追加します。
operator+= | basic_string& operator+=(const string& str) | 自分自身に最後に文字列strを追加します。
operator[] char& operator[](size_type pos) pos番目の文字の参照を返します。
posが無効な場合の動作は未定義です。
※ここに紹介した関数以外のメンバー関数を使うと、size関数、length関数は正しく文字数を 返さなくなります。

    max_size()関数で表される長さよりも長い文字を作ろうとした場合、例外(length_error)が 投げられます。

    上記の関数内で、posなどの範囲が無効な場合、例外(out_of_range)が投げられます。


7章：STL

    stringクラスに関係するオペレーターは以下のものが定義されています。

stringクラスのオペレーター
string operator+(const string& str1, str2);
string operator+(const char* s, const string& str);
string operator+(const char c, const string& str);
string operator+(const string& str, const char* s);
string operator+(const string& str, char c);

bool operator==(const string& str1, const string& str2);
bool operator==(const char* s, const string& str);
bool operator==(const string& str, const char* s);

bool operator!=(const string& str1, const string& str2);
bool operator!=(const char* s, const string& str);
bool operator!=(const string& str, const char* s);

bool operator<(const string& str1, const string& str2);
bool operator<(const char* s, const string& str);
bool operator<(const string& str, const char* s);

bool operator<=(const string& str1, const string& str2);
bool operator<=(const char* s, const string& str);
bool operator<=(const string& str, const char* s);

bool operator>(const string& str1, const string& str2);
bool operator>(const char* s, const string& str);
bool operator>(const string& str, const char* s);

bool operator>=(const string& str1, const string& str2);
bool operator>=(const char* s, const string& str);
bool operator>=(const string& str, const char* s);

istream& operator>>(istream& is, string& str);
istream& getline(istream&is, string& str);
istream& getline(istream&is, string& str, const char delimtor);

ostream& operator<<(ostream& os, const string&);


7章：STL

    例として、TAILプログラムを書き換えてみます。 ここで使用するリングバッファークラスはTAILプログラムと 同じものです。

#include<stdlib.h>
#include"RingBuffer.h"
#include<fstream>
#include<iostream>
#include<string>
using namespace std;

// tail 処理関数のプロトタイプ
void tail( istream& is, int size ) ;

/////////////////////////////////////////////////////////////////////////////
//    使い方表示
void usage( ){
    cerr << "usage : mytail [ -number ] [ file... ]\n" ;
}

///////////////////////////////////////////////////////////////////////////////
//    メイン
int main( int argc, char** argv ){
    // 出力行数：デフォルトは10行
    int output_lineno = 10 ;

    // オプションの取得
    if( argc >= 2 && argv[1][0] == '-' ){
        // 出力行数を獲得する
        output_lineno = atoi( &argv[1][1] ) ;
        if( output_lineno <= 0 ){
            usage( ) ;
            return 1 ;
        }

        // 引数を調整する
        argc-- ; argv++ ;
    }

    // 標準入力から読み込む時
    if( argc == 1 ){
        // 処理本体は tail 関数に任せる
        tail( cin, output_lineno ) ;
    }

    // ファイルから読み込む時
    else{
        for( int i = 1 ; i < argc ; i++ ){
            // 入力ファイルストリームを準備する
            // オブジェクト名はifs
            // ファイル名は順次argvに格納されている
            ifstream ifs( argv[i] ) ;

            tail( ifs, output_lineno ) ;
        }
    }

    // 正常終了
    return 0 ;
}

////////////////////////////////////////////////////////////////////////////
//     tail 処理関数

void tail( istream& is, int size ){
    // 出力行数分のリングバッファ
    // string型のリングバッファを獲得する
    // オブジェクト名はring_buf, サイズは size
    RingBuffer<string> ring_buf( size ) ;

    // 入力行
    string line ;

    // １行ずつリングバッファに読み込む
    while( getline( is, line ) ){
        // リングバッファに詰め込む
        ring_buf.add( line ) ;
    }

    // リングバッファに残っている行を順次出力する
    int length = ring_buf.GetLength();
    for( int i = 0 ; i < length ; i++ )
        cout << ring_buf.get(i) << endl;
}


7章：STL

20. コンテナを作ろう

    コンテナを作るのはそのコンテナがどういうデータの持ち方をするのかがわかれば簡単です。 しかし面倒です。メンバー関数自体を作るのは簡単なのですが、メンバー関数がたくさんあるので、 すべてを作るのが面倒なのです。

    ここでは、vectorクラスを真似た「Vector」クラスを作ってみることにします。Vectorクラスは 動的配列です。つまり必要に応じて大きさを変えることができます。そのデータは隣り合わせで 並んでいるので、イタレーターはランダムアクセスイタレーターをサポートし、ポインタと同等になります。

    push_backなどによりデータが増えた場合で、配列がいっぱいになった場合は、配列の大きさを 10増やします。clearやeraseによって減った場合も、配列の大きさを調整し、現在の要素数+10に 変更します。

Vector.h

#ifndef VECTOR_H
#define VECTOR_H

#include<iostream>
using namespace std;
template<class T>
class Vector{
public:
    typedef T* iterator;
    typedef T& reference;

    // コンストラクタ
    Vector(int vsize=0, const T& v=T() ){
        array_size   = vsize;
        current_size = vsize;
        array        = new T[array_size+1];     //end()が指すiteratorを作るため１を追加する。

        for( int i = 0 ; i < vsize ; i++ )
            array[i] = v;
    }

    // コピーコンストラクタ
    Vector( const Vector& src ){
        array_size   = src.array_size;
        current_size = src.current_size;
        array        = new T[array_size+1];
        for( int i = 0 ; i < current_size ; i++ )
            array[i] = src.array[i];
    }

    // コンストラクタ
    Vector( iterator itr_first, iterator itr_last ){
        array_size  = itr_last - itr_first;
        currentSize = array_size;
        array       = new T[array_size+1];
        for( int i = 0 ; i < current_size ; i++, itr_first++ )
            array[i] = *itr_first;
    }

    // デストラクタ
    ~Vector(){
        delete [] array;
    }

    iterator begin(){   return array; }
    iterator end(){     return array+current_size; }
    int  size(){        return current_size; }
    int  capacity(){    return array_size; }
    bool empty() const{ return current_size==0; }

    // at関数はoperator[]と異なり、チェックつき
    reference at(int index){
        if( size() <= index ){
            cerr << "error" << endl;
            exit(1);
        }
        return array[index];
    }

    reference front(){ return array[0]; }
    reference back(){  return array[current_size-1]; }
    void push_back(const T  &x);
    void pop_back();

    iterator erase(iterator itr);
    iterator erase(iterator first,iterator last);

    void clear(){
        erase( begin(), end() );
    }

    //添え字演算子関数
    reference operator[](int index){ return array[index];}

private:
    void resize(int size);
    iterator array;
    int current_size;
    int array_size;

};

template<class T> void Vector<T>::push_back(const T &x){
    if(current_size >= array_size)
        resize( size() + 10 );

    array[current_size+1] = array[current_size];
    array[current_size]   = x;
    current_size++;
}

template<class T> void Vector<T>::pop_back(){
    array[current_size] = array[current_size+1];
    current_size--;
}

template<class T> Vector&T>::iterator Vector<T>::erase(iterator itr){
    iterator next, tmp;
    iterator return_itr = itr;    //消去した次のiteratorを返すため現在位置を取得

    while( itr != end() ){
        tmp  = itr++;
        next = itr;
        *tmp = *next;
    }

    current_size--;
    return return_itr;
}

//
// 効率の悪いerase
template<class T> Vector<T>::iterator Vector::erase(iterator first, iterator last){
    iterator itr = begin();
    for( ; itr != first && itr != end() ; itr++ );
    while( itr != last  && itr != end() )
        erase( itr );

    resize( current_size+10 );

    return itr;
}

// 配列の大きさをcurrent_size+1に変える
template<class T> void Vector<T>::resize(int size){
    iterator tmp  = new T[size+1];
    iterator tmp2 = tmp;
    iterator itr  = begin();

    for( ; itr != end() ; itr++ )
        *tmp = *itr;

    delete [] array;
    array = tmp2;

    array_size   = size;
}

#endif

main.cpp

#include<iostream>

#include<string>
#include "Vector.h"
#include<algorithm>
using namespace std;

void disp( Vector<int>* p ){
    for( int i = 0 ; i < p->size() ; i++ )
        cout << p->at(i) << "  ";
    cout << endl;
}

void main(){
    int i;

    Vector<int> v;

    // 逆順に格納する
    for( i = 9 ; i >= 0 ; i-- )
        v.push_back( i );

    // 表示
    cout << "v  ";
    disp( &v );

    // コピーコンストラクタ
    Vector<int> v2(v);
    cout << "v2 ";
    disp( &v2 );

    // v2のキャパシティーとサイズ
    cout << "v2のキャパシティー=" << v2.capacity() << "  v2のサイズ=" << v2.size() << endl;

    // v2を削除
    v2.erase( v2.begin(), v2.end() );

    // v2のキャパシティーとサイズ
    cout << "v2のキャパシティー=" << v2.capacity() << "  v2のサイズ=" << v2.size() << endl;

    // vをソート
    sort( v.begin(), v.end() );

    // vを表示
    disp( &v );
}

v  9  8  7  6  5  4  3  2  1  0
v2 9  8  7  6  5  4  3  2  1  0
v2のキャパシティー=10  v2のサイズ=10
v2のキャパシティー=10  v2のサイズ=0
0  1  2  3  4  5  6  7  8  9


7章：STL

21. アルゴリズムを作ろう

    アルゴリズムは、コンテナを知らなくてもいいように記述しなければなりません。また、 テンプレートを利用して、int型、float型、ポインタ型、さらにはクラスや構造体など、 どんな型にも対応させなければなりません。

// T型の要素を探索する
//見つかった時はその要素を指すiteratorを返す。
//見つからなかった時はlastを返す。
template<class InIt,class T>
InIt find(InIt first,InIt last,const T& val){
    InIt itr = first;
    for( ; itr != last ; itr++ ){
        if( *itr == val )
            break;
    }
    return itr;
}
のように記述します。

    それでは自前で作ったアルゴリズムを紹介します。
Algorithm.h

#ifndef ALGOLITHM_H
#define ALGOLITHM_H

#include<iostream>
#include<time.h>

namespace Algorithm{
/*------------------------------------------------------
//OutIt--出力反復子を示す
//intIt--入力反復子を示す
//FwdIt--前方反復子を示す
//BidIt--双方向反復子を示す
//RanIt--ランダムアクセス反復子を示す
//Pred -- 条件式で真か偽を返す
-------------------------------------------------------*/
template<class T>                            void   swap(T& x,T& y);
template<class FwdIt1,class FwdIt2>          void   iter_swap(FwdIt1 x,FwdIt2 y);
template<class FwdIt1,class FwdIt2>          FwdIt2 swap_ranges(FwdIt1 first, FwdIt1 last, FwdIt2 x);
template<class InIt,  class T>               InIt   find(InIt first,InIt last,const T& val);
template<class InIt,  class Pred>            InIt   find_if(InIt first,InIt last,Pred pr);
template<class InIt,  class T>               int    count(InIt first,InIt last,const T& val);
template<class InIt,  class Pred>            int    count_if(InIt first,InIt last,Pred pr);
template<class FwdIt, class T>               FwdIt  remove(FwdIt first,FwdIt last,const T& val);
template<class InIt,  class Fun>             Fun    for_each(InIt first, InIt, Fun f);
template<class InIt,  class OutIt,class Fun> OutIt  transform(InIt first, InIt last, OutIt res, Fun f);
template<class InIt,  class OutIt>           OutIt  copy(InIt first, InIt last, OutIt);
template<class FwdIt, class T>               void   replace(FwdIt first, FwdIt last, const T& vold, const T& vnew);
template<class FwdIt, class Pred, class T>   void   replace_if(FwdIt first, FwdIt last, Pred pr, const T& val);
template<class RandIt>                       void   random_shuffle(RandIt first,RandIt last);
template<class RandIt>                       void   sort(RandIt first,RandIt last);

template<class T>
void swap(T& x, T& y){
    T tmp = x;
    x     = y;
    y     = tmp;
}

//反復子引数が指す要素を交換する
template<class FwdIt1,class FwdIt2>
void iter_swap(FwdIt1 x,FwdIt2 y){
    swap( *x, *y );
}

//firstからlastの要素をxから始まる要素と交換する
template<class FwdIt1,class FwdIt2>
FwdIt2 swap_ranges(FwdIt1 first, FwdIt1 last, FwdIt2 x){
    while(first!=last)
        iter_swap(first++, x++);

    return x;
}

// T型の要素を探索する
//見つかった時はその要素を指すiteratorを返す。
//見つからなかった時は、lastを返す。
template<class InIt,class T>
InIt find(InIt first,InIt last,const T& val){
    InIt itr = first;
    for( ; itr != last ; itr++ ){
        if( *itr == val )
            break;
    }
    return itr;
}

//見つかった時はその要素を指すiteratorを返す。
template<class InIt,class Pred>
InIt find_if(InIt first,InIt last,Pred pr){
    InIt itr = first;
    while( itr != last )
        if( pr( *itr ) )
            return itr;

    return itr
}

//val要素の数を数える
template<class InIt,class T>
int count(InIt first,InIt last,const T& val){
    InIt itr   = first;
    int  count = 0;

    for( ; (itr=find(itr,last,val) ) != last ; itr++ )
        count++;
    return count;
}

//条件を満たす要素の個数を返す
template<class InIt,class Pred>
int count_if(InIt first,InIt last,Pred pr){
    InIt itr  = first;
    int count = 0;

    for( ; itr != last ; itr++ )
        count += pr(*itr);

    return count; //条件に満たした個数を返す
}

//要素valを格納するセルを削除し、戻り値は変更された終端のiteratorを返す
template<class FwdIt,class T>
FwdIt remove(FwdIt first,FwdIt last,const T& val){
    FwdIt itr = first;
    FwdIt current_itr;       //removeするiteratorを指す
    FwdIt root_itr;          //removeする値が連続した場合、連続する最初のiteratorを保持する
    bool  flag = false;      //取り去る値が連続してる時、フラグを立てる。

    while( (itr=find(itr,last,val)) != last) {
        current_itr=itr;
        itr++;                               // 次の要素を知るためiteratorを１つ進める.

        if( *current_itr != *itr ){
            if( flag ){
                *last     = *root_itr;       //取り去る値の最初の位置に連続終端の次に現れる値を交換する
                *root_itr = *itr;
                *itr      = *last;

                root_itr++;                  //連続した最初の位置の次にiteratorを進め、またそこから検索する
                itr  = root_itr;
                flag = false;
            }
            else{
                *last        = *current_itr; //現在位置の要素をエンド領域に格納さしてもらう。
                *current_itr = *itr;
                *itr         = *last;
            }
        }
        else if( !flag ){                    //取り去る値が連続する時
            root_itr = current_itr;
            flag     = true;
        }
    }

    itr = find( first,last,val );    //取り除かれた値は後ろに積められたので
                                     //itrはゴミ領域の最初のiteratorを返す。
    return itr;
}

template<class InIt,class Fun>
Fun for_each(InIt first, InIt last, Fun f){
    while( first != last)
        f( *first++ );

    return f;
}

template<class InIt,class OutIt,class Fun>
OutIt transform(InIt first, InIt last, OutIt res, Fun f){
    while( first != last )
        *res++ = f(*first++);

    return res;
}

template<class InIt,class OutIt>
OutIt copy(InIt first, InIt last, OutIt){
    while( first != last )
        *res++ = *first++;

    return res;
}

template<class FwdIt,class T>
void replace(FwdIt first, FwdIt last, const T& vold, const T& vnew){
    for( ; first != last ; first++ ){
        if( *first == vold )
            *first=vnew;
    }
}

template<class FwdIt,class Pred, class T>
void replace_if(FwdIt first, FwdIt last, Pred pr, const T& val){
    for( ; first != last ; first++ ){
        if(pr(*first))
             *first = val;
    }
}

template<class RandIt>
void random_shuffle(RandIt first,RandIt last){
    int num;
    int count  = 0;
    RandIt tmp = first;

    for( ; tmp != last ; count++, tmp++ );

    srand( (unsigned)time( NULL ) );

    for( ; count > 1 ; first++, count-- ){
        rand();
        num = (int)(count*(rand()/32767.1)+1);
        tmp = first;

        for( ; num > 1 ; tmp++, num-- );

        swap( *first, *tmp );
    }
}

// クイックソート
template<class RandIt>
void sort( RandIt first, RandIt last ){
    last--;

    RandIt i;
    RandIt j;
    RandIt x;

    x = first;                // ピボット
    i = first;                // first は 左境界，lastは 右境界
    j = last;

    while(true) {             //無限ループ
        while( *i < *x )
            i++;              // 交換する要素の決定
        while( *x < *j )
            j--;
        if( i >= j )
            break;            // 左境界が右境界を越えたら ループ終了
        swap( *i, *j );       // i と jを交換
        i++;                  // 境界をさらにせばめる．
        j--;
    }

    if( first < i-1 )
        sort( first, i );     // i, jの変換があれば もう一度行なう
    if( j+1 < last )
        sort( j+1, last+1 );  // +1がポイント
}

};
#endif

main.cpp

#include<iostream>

#include<vector>
#include "Algorthm.h"

using namespace Algorithm;
using namespace std;

// 表示する関数
// これも一種のアルゴリズムと考えられる
template<class InIt>
void disp(InIt first, InIt last){
    for( ; first != last ; first++ )
        cout << *first << ' ';
    cout << endl;
}

void main(){
    vector<int>	v;
    int i;
    for( i = 0 ; i < 10 ; i++ )
        v.push_back( i );

    disp( v.begin(), v.end() );

    random_shuffle( v.begin(), v.end() );
    disp( v.begin(), v.end() );

    sort( v.begin(), v.end() );
    disp( v.begin(), v.end() );
}

0 1 2 3 4 5 6 7 8 9
6 1 5 9 8 4 2 7 3 0
0 1 2 3 4 5 6 7 8 9


7章：STL

22. 配列とアルゴリズム

    配列はもっとも単純で、メンバー関数の定義されていないコンテナと見ることができます。 したがって、既存のアルゴリズムを用いることもできるのです。その簡単な例を紹介します。

#include<iostream.h>
#include<algorithm>
using namespace std;

void print(int n){
    cout << n << ' ';
}

void main(){
    // 配列を準備
    int array[10];

    // イタレーターの準備
    // itr1 = array.begin()に相当
    int *itr1 = array;

    // itr2 = array.end()に相当
    int *itr2 = array + sizeof(array) / sizeof(int);

    // 配列に0～9を代入
    for( int i = 0 ; i < 10 ; i++ )
        array[i] = i;

    // 既存のアルゴリズム
    random_shuffle(itr1, itr2);

    // すべてを表示
    for_each( itr1, itr2, print );

    // 改行
    cout << endl;

    // ソート
    sort( itr1, itr2 );

    // すべてを表示
    for_each( itr1, itr2, print );
}

4 3 0 2 6 7 8 9 5 1
0 1 2 3 4 5 6 7 8 9


6章：継承

← 前章へ　:　 トップへ　:　 次ページへ →

8章：その他

1. 変数名について

    決まり事ではありませんが、慣用的な変数名の付け方があります。慣用的な変数名の 付け方は時代とともに変わってきますが、ここではその一部を紹介します。

    変数名はC++になって、int型、char型、ポインタ型のような基本型(プリミティブな型と言います)以外にも クラスのインスタンスやクラスのポインタなど多岐にわたるようになりました。したがって ぱっと見ただけでどんな型を表しているのかがわかるように付けたほうが便利です。そこで、 以下のような変数名の付け方が慣用的に用いられるようになりました。

スコープによる接頭語
スコープ | 接頭語
クラスのメンバー変数 | 「m_」で始める
スタティック変数 | 「s_」で始める
グローバル変数 | 「g_」で始める
オート変数(ローカル変数) | 何も付けない

型による接頭語
型 | 接頭語
数値 | 「n」で始める
フラグ | 「f」で始める
フラグ以外のbool型 | 「b」で始める
文字(char型) | 「ch」で始める
クラスのポインタ | 「cp」もしくは「p」で始める
ポインタ | 「p」で始める
32bitのポインタ | 「lp」で始める
文字列(char*型またはstring型) | 「str」、「lp」もしくは「p」で始める
vector | 「vct」で始める
list | 「lst」で始める

例
メンバー変数の文字列 | m_strTitle
グローバル変数の数値 | g_nLength

    少し前までは、変数名や関数名に大文字を用いることは少なかったのですが、 最近は大文字を混ぜたほうが見やすいことから、大文字を取り入れる場合も多々あります。

    また、#defineやenum{...};で定義する定数などは、すべて大文字にするケースも 多いです。

← 前章へ　:　 トップへ　:　 次ページへ →


C++入門

    C++入門へようこそ。

    C、C++のコンパイラは購入すると結構な出費になります。将来プログラマーを目指したり、 シェアウェアーやフリーウェアーを作る人ならまだしも、これから勉強してみようかなと思う 学生がコンパイラを買うのはちょっと痛い出費かもしれません。そこで、フリーで手に入る コンパイラを紹介します。

  • Borland C++ 5.5
  • LSI C-86 Ver.3.30c試食版
  • 技術評論社、C言語スタートブック 付録CD-ROMなど
    また、私がよく読むC++の本を紹介します。今手に入るかどうかはわかりませんが。。。
出版社 | タイトル | ISBN | 内容
翔泳社 | 独習C++ 　 | 初心者むけ
マイクロソフト MSDNライブラリ
初級者から上級者まで
アスキー出版 | プログラミング言語 C++ 第三版 | 4-7561-1895-X | 中級者から上級者向け
本の検索・ご購入は「 コンピュータの本・専門店」で。

←目次から選んでください

・ トップページに戻る


C++入門

トップ->C++入門

あなたは

人目のC++(C)言語入門受講生です。

目次
0. はじめに

1. オブジェクト指向とは？
      1. オブジェクト指向とクラス
      2. 継承
      3. カプセル化
      4. ポリモーフィズム

2. ストリーム
      1. 出力
      2. マニピュレータ
      3. 入力
      4. ファイル
      5. 練習問題1
      6. 文字列
      7. 練習問題2

3. C++の新しい文法
      1. 新しい型bool
      2. デフォルト引数
      3. newとdelete
      4. 参照型
      5. const
      6. 変数の宣言
      7. 例外
      8. オーバーロード
      9. テンプレート関数
      10. 名前空間

4. クラス
      1. クラスとは
      2. クラスの宣言
      3. クラスの実装
      4. コンストラクタとデストラクタ
      5. クラスの使用法
      6. 例題）スタッククラス
      7. テンプレートクラス
      8. 練習問題
      9. 参照型
      10. 代入演算子
      11. コピーコンストラクタ
      12. 構造体
      13. メンバー変数の初期化
      14. 内部クラス
      15. 無名クラス
      16. 無名共用体
      17. 演算子の作り方
      18. friend
      19. 練習問題
      20. クラス変数(静的変数)
      21. 静的関数
      22. クラスと関数ポインタ

5. クラスの包含
      1. 包含とは
      2. クラスの作成・破壊
      3. メンバーイニシャライザ
      4. ポインタによる包含
      5. 参照による包含
      6. 練習問題

6. 継承
      1. 継承とは
      2. スーパークラスのコンストラクタ
      3. 継承とキャスト
      4. スコープ
      5. クラスの作成・破壊
      6. 派生の種類
      7. 仮装関数
      8. 純粋仮装関数
      9. 仮装デストラクタ
      10. 例題）例外クラス
      11. V-table(VF-table)
      12. 例題）お絵かきソフト
      13. 継承と包含
      14. 多重継承
      15. 多重継承の用途
      16. 仮想クラス
      17. 実行時型情報(RTTI)
      18. dynamic_cast

7. STL
      1. STLとは
      2. STLの歴史
      3. STLの構成
      4. コンテナ
      5. vector
      6. イタレーター
      7. クラスとSTL
      8. list
      9. queue
      10. deque
      11. priority_queue
      12. stack
      13. map
      14. mutimap
      15. set
      16. multiset
      17. bitset
      18. アルゴリズム
      19. basic_string
      20. コンテナを作ろう
      21. アルゴリズムを作ろう
      22. 配列とアルゴリズム

8. その他
      1. 変数名について

・ トップページに戻る


はじめに

    Java入門へようこそ。

    JavaはWindowsやMacOS、UNIXさらには携帯電話やPalmなどのようなPDAなどさまざまなプラットフォーム で使用できるように作られたプログラム言語です。CPUの種類が異なっても使用できるということは すごいことです。

    一般にC言語のようなプログラムは、人間が作ったプログラムをCPUが理解できる言葉に翻訳 します。これをコンパイルといいます。Javaでは、CPUが理解できる言葉に翻訳するのではなく、 Java Virtual Machine(JVM)が理解できる言葉(中間言語＝バイトコード)にコンパイルします。WindowsではWindows用の JVM上で、UNIXではUNIXで動作するJVM上で、このコンパイルされたバイトコードが実行されるのです。 JVMは、コンパイルされたプログラムをWindowsなりUNIXが理解できる言葉に、さらに翻訳されて 実行されるのです。このようにしてさまざまなプラットフォーム上で動作することができます。 コンパイルされたバイトコードはclassファイルといい、当然WindowsでもUNIXでも同じファイル ができあがるため、WindowsでコンパイルされたclassファイルをUNIXで実行することも可能です。 したがって、Windowsで作られたJava AppletをMacで実行することもできるのです。

    Javaは、プログラムしやすいように、便利なクラスが標準で用意されています。例えば、 GUIを作ろうとした場合、ボタンを担うプログラム(クラス)、テキストを入力するための テキストエリアを担うプログラム(プログラム)、さらには通信を担うプログラム(クラス)が 用意されているのです。プログラマーは用意されたプログラム(クラス)をつなぎ合わせるだけで プログラムを作っていくわけです。

    標準で用意されているクラスはJava2と呼ばれるバージョンで1500を超えます。さらに、 ほかの人が作ったクラスをあわせると膨大な数になります。こんなに解説することは無理なので、 ここではJavaの基本と、有用なクラスと、ブラウザ上で実行可能なJava Appletを解説します。

    Java Appletとは、IEやNetscape Navigatorなどのブラウザ上で動作するJavaのことで、 一般にJava2に対してJava1と呼ばれるバージョンが動作します。最新のブラウザではJava2の 一部が動作するようです。古いブラウザでもプラグインをインストールすればJava2の一部が 動作可能ですが、ここでは汎用性を考慮し、Java1の解説を行います。

    Java2はJava1を機能拡張したもので、バグが少ないのと、便利なクラスが追加されています。 本ホームページでは「Java入門」ですので、基本的なことを学ぶのが目的ですので、 Java1で充分だと思います。基本的なことを学んだあとは、必要によって解説書や、他のホームページ などでJava2に挑戦していってもらいたいと思います。

← 前ページへ　:　 トップへ　:　 次ページへ →


1章：Javaの基礎

1. コンパイル方法・実行方法

    まず、コンパイル方法を解説します。JBuilder など統合環境の開発ソフトを使用している方は、そちらのマニュアルをお読みください。ここでは、 JDKを使用して 解説します。

    まず、Javaのプログラムを書いてみます。何も考えずに下記のプログラムを書いてみてください。 ファイル名も同じにしてください。後で解説しますが、Javaではファイル名のつけ方も決まっているのです。
Test.java

import java.io.*;

class Test{
    public static void main(String args[]){
        System.out.println("Hello Java World.");
    }
}
    プログラムを学ぶ一番最初は文字を表示を表示するものだと、昔から決まっていますので、 ここでも「Hello Java World」という文字を表示するプログラムを作ってみました。

C:\>Java>javac Test.java

C:\>Java>java Test
Hello Java World.

    イタリックの部分が入力する部分です。

もし、入力間違えがあれば、下記のようにエラーメッセージが表示されます。

C:\>Java>javac Test.java
Test.java:4: Can't specify array dimension in a declaration.
    public static void main(String args[1]){
                                        ^
1 error

C:\>Java>

    これは「args[1]」と、括弧の中に数字の１を間違って入力した場合です。

    エラーが多いと、エラーメッセージが上に消えてしまいます。Windows NT, 2000, XPを利用している 人は、DOS窓の上に表示を戻すことができますが、Windows 95, 98(SE), MeなどのWindowsを利用している 人は、消えてしまったものは、二度と見ることができません。その場合は、統合環境の開発ソフトを 利用するか、JCを利用 してください。

    JCを利用すると、エラーメッセージをエディターで表示することができますので、上に消えて しまう心配がありません。


1章：Javaの基礎

2. 予約語

    自分で作る変数や関数などの名前は「識別子」と呼びます。識別子の名前は基本的にはプログラマーが 自由に命名できます。ただし次のような制約があります。

  • 半角英数字および半角アンダーバー(_)のみ使用でき、最初の1文字は英字である
  • 大文字、小文字は区別される
  • 以下の予約語は使用できない
予約語
abstract | boolean | break | byte
cast | catch | char | class
const | continue | default | do
double | else | extends | final
false | finally | float | for
goto | if | implements | import
instanceof | int | interface | long
native | new | package | private
protected | public | return | short
static | super | switch | synchronized
this | throw | throws | transient
true | try | void | vlatile
while

    文法的に決められている決まりは以上ですが、慣用的に使われる識別子の命名法があり、 これは広く守られている慣用命名法ですので、ここで解説します。

  • クラス名は大文字ではじめる
  • メソッド(C言語で言う関数)、変数は小文字ではじめる
  • メソッド(C言語で言う関数)、変数は単語のつながりで表現し、２単語目以降の単語の初めは大文字にする
例）
class MyClass{
    int  lengthOfLine;
    int  getLength(){
        return lengthOfLine;
    }
}

イタリックがクラス名、アンダーラインは変数、メソッド名

    C++を経験しているプログラマーはさらに以下のことを注意して識別子を命名している人もいます。

  • 変数はデータ型を最初に付する
  • メンバー変数は「m_」ではじめる
例）
class MyClass{
    int     m_nLengthOfLine;
    boolean m_bDataFlag
}

    本ホームページでは基本的に以上の命名法をもとに、クラス名、変数名を命名します。 また、プログラムでは予約語、Javaで用意されているクラスは色を変えて表現します。
Test.java

import java.io.*;

// コメント
class Test{
    public static void main(String args[]){
        System.out.println("Hello Java World.");
    }
}

  緑　　　： コメント
水　色　　： 予約語
グレー　　： JDKで用意されているクラス


1章：Javaの基礎

3. 構成

    Javaを文法的に見た場合の構成について説明します。

語句の種類
    C言語を構成する語句は「識別子」、「予約語」、「定数」、「文字列」、「演算子」、 「区切子」、「コメント」、「ブランク」があります。

    識別子は、メソッド名、変数名そしてクラス名のことです。変数には、 単純変数、配列の２種類があり、さらにint型、char型などの型が存在します。

    予約語は、プログラムの実行順序の制御や、変数の型など、決まっている語です。

    定数はその名の通り、定数です。文字列も定数(文字)の羅列で、定数の１種と考えられます

    演算子は足し算、引き算などの「+」や「-」のことです。

    区切子とは、文と文を区切ったりするものです。

    コメントとは、何をしている部分だとか、この関数は何をするものだとか、ここはまだ 未完成だよとか、プログラムとは関係ないが、プログラマーが自分あるいは、一緒に作っている 仲間のプログラマーに対してのメッセージです。コメントの書き方は２種類あります。１つは 「//」の以降改行まで、２つ目は「/*」と「*/」の間です。 後者は入れ子ができません。

    ブランクとは、プログラムを見やすくするための改行や、タブ、スペースを言います。

式
    これらの語句を組み合わせて式を構成します。

    式には一次式と演算式(演算子を用いて一次式を組み合わせたもの)に分けることができます (数学で言う一次式とは異なります)。

    関数も式になります。何かを処理するための引数を必要とし、その結果を返します。 その結果を戻り値と言います。

文
    式１つもしくは複数の式から文を構成します。文の後には必ず１文の終わりを示す記号 「;」がつきます。１行の中に文が２つ以上あっても構いません。エディターで見た場合に １行の中に複数の文があっても、コンパイラは１文を１文として認識するからです。

    したがって、次の左のプログラムと右のプログラムは同じものです。
import  java.io.*;

class Test{
    public static void main(String args[]){
        int x = 10;
        int y = 20;
        int z = x + y;
    }
}

import  java.io.*;

class Test{
public static void main(String args[]){
int x = 10;   int y = 20;   int z = x + y;
}}

    文には「式の文」(上で言う「int x = 10;」など)、空文(「;」のみからなる文)、 制御文(「break;」、「return;」など)があります。

ブロック
    文が0文、１文、あるいは複数の文からブロックが構成されます。文をブロックを表す 「{」と「}」で囲みます。ブロックは「１文と見なす単位」のことです。

    条件分岐(「if」、「else」)や反復(「for」や「while」など)などは、基本的に１文しか 実行できません。しかし１文だけしか繰り返せないのでは不便です。そこで このブロックで１文と見なすことで、複数の文を反復することができるのです。

    また、ブロックは入れ子にできるので、複数のブロックを１つのブロックとすることも できます。

    JavaはC++と同様オブジェクト指向言語です。オブジェクト指向とは、プログラムも実世界に存在する モノと考えてプログラムを作ることです。このモノをクラスと呼びます。クラスはデータとメソッド (C++では関数)から作られます。を例えば、オブジェクト指向言語でお絵かきソフトを作ろうとした場合、 ペンクラスを用意し、ペンクラスには「絵を書く」というメソッドなどを用意します。

    哺乳類を考えた場合、哺乳類クラスには「餌を食べる」「寝る」などのメソッドと、「足の数」という データを用意します。犬は哺乳類の一種なので、「犬」クラスは「哺乳類」クラスのサブクラスと呼ぶことが でき、この場合「足の数」は４になります。これを継承といいます。このように継承を使用すると、 新しいクラス(ここでは犬クラス)を作る場合に、その差分だけを作ればよいので、プログラムの効率が よくなります。

    ところで、先ほどのペンですが、ペンは文房具の一種ですが、文房具にははさみなどもありますので、 差分だけ作るというのは無理なようです。はさみクラスは仕方がないので、新しく作らなければなりませんが、 その他にもクラスが増えてくると、文房具は文房具でなんとかひとまとめにしたいです。

    このような場合、C++では「名前空間(namespace)」という機能を作ることでとりあえず分別はできますが、 実質は名前空間を分けているだけなので、分別しているわけではありません。Javaでは、パッケージという 機能を使うことで、このような場合、文房具は文房具でまとめることができます。

    もっと具体的に言うと、JDKで用意されているクラスのうち、GUIを担うクラス郡(ボタンやテキストエリアなど) は「awt」という名前でパッケージ化されています。ネットワーク関係を担うクラス郡は「net」という 名前でパッケージ化してあります。さらに、パッケージの中にパッケージを入れることもできますので、 たくさんのクラスを作った場合、整理するのがC++に比べて便利になっています。

    また、JavaにはCやC++のようなヘッダーファイルがありません。したがってJavaには「#include」は ありませんが、どのパッケージを使用するかをあらかじめ宣言しなくてはなりません。それが下の例の 「import」です。下の例では、javaパッケージのioパッケージの中の全てのクラスを使用できるように というimport宣言です。(*が全てのクラスを使用するという意味)

Test.java

import java.io.*;

// コメント
class Test{
    public static void main(String args[]){
        System.out.println("Hello Java World.");
    }
}

    Javaでは、すべてプログラムはクラス内に収められます。これがC++とは大きく違うところのひとつです。 したがって、「class Test」というように、クラス宣言があります。

    そして、クラス内に最初に実行されるべきmainメソッドを定義します。CやC++では、main関数を 定義するのに、引数を取ったり取らなかったり、また、戻り値をintにしたりvoidにしたり自由に 定義できましたが、Javaでは、戻り値はvoid、引数はString型の配列と決まっていますので、 このように記述します。

    そして、必要ならば他のメソッドや変数を定義します。

    また、Javaでは基本的に１つのファイルに１クラスです。クラス名とファイル名は同じです。 拡張子は「java」にします。特殊な場合は１ファイルに複数のクラスを定義することもできますが、 オブジェクト指向に反するので、このような使い方は一般にしません。小さなプログラムや、 Java Appletを作るときには、やりますが・・・。


1章：Javaの基礎

4. データの型と宣言

    変数を使う場合には宣言をしなくてはなりません。何を宣言するかというと、 どんなデータの型かと、名前、初期値です。初期値は省略することができます。

    どこで宣言をするかというと、C言語とはことなり、C++と同様どこでも宣言できます。 クラス内のメソッド外で宣言すると、メンバー変数と呼ばれる変数になります。 メソッドの中で宣言するとローカル変数と呼ばれる変数になり、そのメソッドの中でのみ 有効になります。

    JavaではCやC++と異なり、全て符号付になります。unsignedのような符号なし整数は 存在しません。

    次にデータの型について説明します。Javaでは次の型が用意されています。

データの種類 | 型 | バイト長 | 数値の範囲
真or偽 | boolean | true or false
整数 | byte | 1 | -128～+127
1文字 | char | 2 | -32768～+32767
整数 | short | 2 | -32768～+32767
整数 | int | 4 | -2147483648～+2147483647
整数 | long | 8 | -263～263-1
小数 | float | 4 | 仮数部232、指数部28
小数 | double | 8 | 仮数部252、指数部211
    CやC++と異なり、１文字をを表す「char」型は、16bit(2byte)に拡張されています。 これはJavaは世界中の言語で使用できるように、例えば漢字も１文字で表すことができるよう unicodeと呼ばれる文字コードに拡張されているためです。

int   a;		// 変数名「a」をint型で宣言
char  ch1, ch2;	// 変数名「ch1」と「ch2」をchar型で宣言
long  l;		// 変数名「l」をunsigned long型で宣言
    変数の宣言を行っただけだと、その変数にはどんな値が 入っているかわかりません。宣言後、代入もなしに使用しようとするとエラーが出ます。 (次ページ参照)


1章：Javaの基礎

5. 定数と変数

    宣言した変数に値を代入する方法を説明しますが、その前に定数について説明します。

整数定数
    整数定数には次のような種類があります。
・10進数		100	255	-50
・16進数		0x64	0xFF
・8進数  		0144	0377
・文字  		'd'	'あ'
    このように8進数を表す場合には、最初に「0」を、16進数で表す場合には 「0x」をつけます。文字は１文字です。CやC++では、ひらがなや漢字のような全角文字は ２バイトになるため、１文字とは考えませんでした。しかしJavaでは漢字だろうがひらがな だろうが、１文字は１文字なのです。

    また、long型として定義する場合は、数字の後に「l」または「L」をつけなくては なりません。

小数定数
    小数の場合は普通の書き方ともう一つの方法があります。普通の書き方は、 「1.234」、「-0.364」のような書き方です。

    しかしこの書き方では100万や100万分の一などを書きたい場合、この書き方は 不便です。そこで「1e6」や「1E-6」のように、10の何乗かで表現できます。 「e」と「E」はどちらでも構いません。

    したがって、円周率を表すには、「3.1415」「0.31415E1」、「31415E-5」など と表すことができます。

    また、小数は標準でdouble型になりますので、float型として扱いたい場合は、 数字の後に「f」または「F」をつけなくてはなりません。

代入
    宣言した変数に対して、定数を代入することができます。
    int a, b;		// 変数名「a」「b」をint型で宣言
    char ch1, ch2;	// 変数名「ch1」と「ch2」をchar型で宣言
    long l;		// 変数名「l」をlong型で宣言
    float f;  	// 変数名「f」をfloat型で宣言

    ch1 = 'A';
    ch2 = 0x4C;	// 16進数で4Chを代入
    l = 'C';  	// long型lに'C'のASCIIコード4Chを代入
//	f = 3.1415;	// エラー！！
    f = 3.1415f;	// fを最後に付ける
    a = b = 100;	// aとbに100を代入
    一番最後の「a = b = 100;」は実は複雑な使い方です。詳細は 後述します。ここではaにもbにも100が代入されると覚えておいて ください。

初期化
    変数を宣言しただけだと、どんな値が入っているかわかりません。変数の宣言と 同時にある値で初期化することもできます。
int a = 100;
char ch = 'あ';
int b = a = 5;   	// aもbも5になる
int n = m = 100;  	// このような使い方はダメ!!

    変数を宣言しただけだと、どのような値が入っているかわかりません。宣言と同時に初期化を せず、さらに代入もしないでその変数を使おうとするとエラーが出ます。

import java.io.*;

class Test{
    public static void main(String args[]){
        int a;
        int b = a;	// どんな値が入っているかわからないaでbを初期化することはできない
    }
}

C:\java\>javac Test.java
Test.java:6: Variable a may not have been initialized.
                int b = a;
                        ^
1 error

    この様なエラーがコンパイル時に出ることによって、CやC++では気がつかなかったバグを 未然に防ぐことができます。


1章：Javaの基礎

6. 簡単な演算

    演算は式と式を演算子でつなぎます。Javaの演算子は非常に多くの種類があります。 詳しくは5章を参照してください。ここでは簡単なものを 紹介します。

代入演算子
    コピー演算子とも言います。変数に定数を代入したり、変数に変数に代入する場合に 用います。他の多くのコンピュータ言語と同じように「=」を用います。

    「char」型の変数の値を「int」型に代入する場合のように、大きい変数に小さい変数 に代入する場合は問題ありません。しかしこれとは逆に「int」型の変数の値を「char」型 の変数に代入する場合、エラーが出ます。CやC++では、下位の値がそのまま入りますが、 バグの原因のひとつですので、Javaではエラーになります。

    このような場合は明示的にキャスト(型変換)をする必要があります。キャストについては 後述します。

 int  n1, n2, n3;
 char c1, c2, c3;

 c1 = 100;
 n1 = c1;		// 代入先が型の方が大きいので問題なし

 n2 = 100;
 c2 = n2;		// エラー：小さい型には代入できない
 c2 = (char)n2;	// 明示的なキャスト/FONT>

算術演算子
    算術演算子とは、「+」「-」「*」「/」「%」です。それぞれ、加算、減算、乗算、 除算、余りを求めるです。整数同士のわり算の場合、結果も整数になります。この場合、 小数点以下は切り捨てになります。また小数での計算で余りを求めることはできません。

    これらの計算の順序は、数学の場合と同じです。

    また、負数を含む「余り(%)」を計算する場合は、余りも負数になります。

比較演算子
    等値か「==」、非等値か「=!」、大小関係「>」「>=」「<=」「<」を調べます。
演算子 使用例 　意　味
< a < b aはbより小さい
<= a <= b aはb以下
> a > b aはbより大きい
>= a >= b aはb以上
== a == b aはbと同じ
!= a != b aとbは異なる
    成り立つ場合は「真」すなわちtrueが、成り立たない場合は「偽」すなわちfalseとなります。

CやC++では、真は１、偽は０となりましたが、Javaではtrueかfalseです。すなわち、この比較結果は boolean型にしか代入できません。明示的なキャストをしても、int型など他の型には代入できません。

 int  n1 = 100;
 int  n2 = 200;
 int  n3;

 n3 = (n1 == n2);		// エラー：型が異なる
 n3 = (int)(n1==n2);	// エラー：キャストしてもだめ
 boolean  b = (n1==n2);	// これはOK

比較の「==」と代入の「=」を間違わないように気をつけてください。初心者がよく間違える 部分です。

論理演算子
    真と偽を論理演算で評価します。
演算子 使用例 　意　味
&& a && b AND(かつ)
|| a || b OR(または)
! !b NOT(否定)
 int  n1 = 100;
 int  n2 = 200;
 int  n3 = 10;

 boolean  b = (n1==n2) && (n1==n3);// n1,n2,n3がすべて同じ時、bはtrueになる

    これら論理演算子は、trueかfalseを演算しますので、boolean型の物しか演算できません。 つまり、CやC++のように以下の使い方はJavaではエラーになります。

 int  n1 = 100;
 int  n2 = 200;
 int  n3 = 10;

 boolean  b1 = n1 && n2;	// エラー
 boolean  b2 = !n2;	// エラー


1章：Javaの基礎

7. ディスプレイへの表示

    結果をDOS窓上に表示させることはアプリケーションとしても行いますし、現在の変数の値を 表示させて、デバッグ作業の一環としてもやります。

    DOS窓への文字の表示は下記のように行います。ここでは、決まり文句だと覚えて置いてください。

import  java.io.*;

class Test{
    public static void main(String args[]){
        System.out.print("abcdef");
        System.out.print("0123");
    }
}

C:\java>javac *.java

C:\java>java Test
abcdef0123
C:\java>

    「System.out.print」の引数に、文字列を渡してやります。改行はしませんので、「abcdef」と「0123」は １行に表示されます。CやC++と同じように改行する場合は「\n」を入れます。

import  java.io.*;

class Test{
    public static void main(String args[]){
        System.out.print("abcdef\n");
        System.out.print("0123\n");
    }
}

C:\java>javac *.java

C:\java>java Test
abcdef
0123

C:\java>

    一番最後に改行を入れる場合は、「\n」と書かなくても「println」メソッドを 使うと改行されます。

import  java.io.*;

class Test{
    public static void main(String args[]){
        System.out.println("abcdef");
        System.out.println("0123");
    }
}

C:\java>javac *.java

C:\java>java Test
abcdef
0123

C:\java>

    DOS窓には「標準出力」と「標準エラー出力」というのがあります。標準出力は、MS-DOSの リダイレクト機能を使うとファイルに落としたりすることができます。標準エラー出力では リダイレクト機能は(普通のやり方では)使えません。

    「System.out.print」は、標準出力に出力します。標準エラー出力に表示したい 場合は、「System.err.println」とします。

import  java.io.*;

class Test{
    public static void main(String args[]){
        System.out.println("abcdef");
        System.err.println("0123");
    }
}

C:\java>javac *.java

C:\java>java Test > a.txt	←リダイレクト機能：標準出力をファイルに
0123			標準エラー出力の未表示される

C:\java>type a.txt		←出力したファイルを表示
abcdef			標準出力のみがファイルに落ちた

C:\java>

    printメソッドは、文字列だけでなく、数字も表示することができます。また、数字や文字列を 「+」演算子でつなげることもできます。

class Test{
    public static void main(String args[]){
        int  n1 = 100;
        System.out.println("n1=" + n1 + "です");
    }
}

C:\java>javac *.java

C:\java>java Test
n1=100です

C:\java>


1章：Javaの基礎

8. 文字列

    C言語では文字列という概念はほとんどなく、ダブルクォーテーションで囲んでも文字の集まりで、 最後にASCIIコードで0という値が付加されるだけでした。C++ではSTL(標準テンプレートライブラリ)で、 stringクラスと言うクラスが追加され、さらにMFCではCStringクラスが追加されたため、文字列という 概念ができてきましたが、基本はまだ文字の集まりでした。Javaではさらに拡張され、文字列という 概念ができあがりました。Javaでは文字列を扱うのに、「String」クラスを用います。

    Javaでは文字をダブルクォーテーションで囲むと、それは文字列となり、Stringクラスの インスタンスになります。文字列は＋演算子で足す(つなぎ合わせる)ことができます。また、 数字なども足すことができます。

import  java.io.*;

class Test{
    public static void main(String args[]){
        int    n         = 1;
        String str1      = "モーニング娘。の中で" + n + "番目にかわいいのは";
        String strMyouji = "後藤";
        String strNamae  = "真紀";
        String strName   = strMyouji + strNamae;
        System.out.println(str1 + strName + "だと思います");
    }
}

C:\java>javac *.java

C:\java>java Test
モーニング娘。の中で1番目にかわいいのは後藤真紀だと思います

C:\java>

    数字を足すことができるのであれば、10進数以外の例えば16進数で表示させたいと思うことが あると思います。これはちょっと複雑になるので、後述します。

    CやC++と同様に「\」(UNIXなどではバックスラッシュ)の後に、特殊な記号を追加することで、 特殊な文字を表示することができます。

エスケープシーケンス | 機　能
\n | 改行(次の行の先頭に行く)
\t | 水平タブ
\\ 円記号「\」(機種・OSによってはバックスラッシュ)
\' シングルクォーテーション「'」
\" ダブルクォーテーション「"」
\b | １文字戻る
\r | その行の先頭に戻る
\uxxxx | xxxxは４桁のUnicodeを表す
import  java.io.*;

class Test{
    public static void main(String args[]){
        int    n         = 1;
        System.out.println("私の\t名前は\"Mike Davis\"\nです。");
    }
}

C:\java>javac *.java

C:\java>java Test
私の    名前は"Mike Davis"
です。

C:\java>

    Unicodeとは下表のような４桁の文字コードです。DOS窓では表示できない文字もあります。


2章：制御文

1. 順次構造

    制御文とは、プログラムの流れを制御するものです。今までの例でもお分かりのように、 C言語も他の多くのプログラム言語と同様、上から下にプログラムは実行されます。 このように上から下に実行される流れを「順次構造」といいます。しかし、 ある処理を繰り返したり、条件によって異なる処理をさせたい場合は、順次構造 以外の流れが必要になります。

    このようなプログラムの流れは以下のような物があります。

  • 順次構造
  • 分岐構造
    • 単一分岐構造
    • 多重分岐構造
    • ケース構造
  • 反復構造
    • 前判定型反復構造
    • 後判定型反復構造
    C++言語やJava言語ではこれに加えて「例外」という機構がありますが、 ここでは省略します。

    アルゴリズムのページも参照してください。

2-1. 順次構造

    上述のように、上から下にかけて実行されるという、もっとも単純な構造です。 フローチャートでいうと右図のようになります。


2章：制御文

2. 単一分岐

    分岐の条件が１つだけの場合を単一分岐という。フローチャートの書き方は以下のようになる。 また、C言語の書き方を青で示した。

変数nが0なら処理1を、
0以外なら処理2を実行する 		nがm以上なら処理1を、
未満なら処理2を実行する
    if(n==0){
        処理1;
    }
    else{
        処理2;
    }

    if(n>=m){
        処理1;
    }
    else{
        処理2;
    }

charの内容が'A'であれば処理1を実行し、
それ以外ならば何もしない
同上
こういう書き方もある

    if(ch=='A'){
        処理1;
    }
    else{
        処理2;
    }

    どの例もすべて、処理をし終わった後は合流していることが重要です。 これは多重分岐などの場合も含めて、必ず合流しなくてはなりません。

    次に例を示します。下の例では、キーボードから数字を入力し、それが10以上 20未満なら「10代」と表示します。

import  java.io.*;

class Test{
    public static void main(String args[]){
        int n;
        n = readNumber();
        if ( 10 <= n && n < 20 )
            System.out.println("10代");

    // キーボードから数字を入力するメソッド
    public static int readNumber(){
        byte b[] = new byte[100];

        try{
             System.in.read(b);
             return Integer.parseInt((new String(b)).trim());
        }catch(Exception e){
             return 0;
        }
    }
}
    上の例のように、「10以上」という条件と「20未満」という条件を 「&&」で区切ることで、 その両方を満たす場合に限り、次の文が実行されます。

    さらに、複数の条件のうちどれか一つでも満たされていれば実行させたい場合は、 「&&」の代わりに、「||」で区切ります。

    また、条件が満たされない場合の処理がなければ、「else」文を省略することも できます。


2章：制御文

3. 多重分岐

    多重分岐とは、単一分岐を組み合わせたもので、単一分岐の処理の中にまた条件分岐を 含むものである。

Javaでの表記

if(条件1){
    if(条件2){
        処理3;
    }
    else{
        処理2;
    }
}
else{
    処理1;
}

    以下に例を示します。以下の例では、キーボードから数字を入力し、その数字が 負数なら「負数」、0なら「0」、整数なら「整数」と表示する。２例を示すが、 どちらも同じ結果になる。

import  java.io.*;

class Test{
    public static void main(String args[]){
        int n;
        n = readNumber();
        if ( n < 0 ){
            System.out.println("負数");
        }
        else{
            if( n == 0 ){
                System.out.println("0");
            }
            else{
                System.out.println("正数");
            }
        }
    }

    // キーボードから数字を入力するメソッド
    public static int readNumber(){
        byte b[] = new byte[100];

        try{
             System.in.read(b);
             return Integer.parseInt((new String(b)).trim());
        }catch(Exception e){
             return 0;
        }
    }
}

import  java.io.*;

class Test{
    public static void main(String args[]){
        int n;
        n = readNumber();
        if ( n < 0 ){
            System.out.println("負数");
        }
        else if( n == 0 ){
            System.out.println("0");
        }
        else{
            System.out.println("正数");
        }
    }

    // キーボードから数字を入力するメソッド
    public static int readNumber(){
        byte b[] = new byte[100];

        try{
             System.in.read(b);
             return Integer.parseInt((new String(b)).trim());
        }catch(Exception e){
             return 0;
        }
    }
}
    上の例のように、多重分岐での「else」は直前の「if」に対応します。


2章：制御文

例題

変数a,b,cに適当な数が代入されている。この変数中で最小値を変数wに代入する。

  考え方：
aとbを比べる。そのうち、小さい方とcを比べる。その結果小さい方が最小値である。

  下のアンダーラインを埋めよ。
  • a≦bでかつa≦cの時　→　最小値は_______________
  • a≦bでかつa＞cの時　→　最小値は_______________
  • a＞bでかつb≦cの時　→　最小値は_______________
  • a＞bでかつb＞cの時　→　最小値は_______________


2章：制御文

例題

変数a,b,cに適当な数が代入されている。この変数中で最小値を変数wに代入する。

  考え方：
aとbを比べる。そのうち、小さい方とcを比べる。その結果小さい方が最小値である。

  • a≦bでかつa≦cの時　→　最小値は　a
  • a≦bでかつa＞cの時　→　最小値は　c
  • a＞bでかつb≦cの時　→　最小値は　b
  • a＞bでかつb＞cの時　→　最小値は　c

これをアルゴリズムで表すと、
if(a<=b){
    if(a<=c)
        w=a;
    else
        w=c;
    }
else{
    if(b<=c)
        w=b;
    else
        w=c;
    }
}


2章：制御文

    前のページでは、多重分岐を用いて最小値を求めた。しかしこのような方法では、 ３つの変数から最小値を求めるだけで、条件分岐を３つも用いており、変数が増えた場合 とんでもないことになってしまう。そこで「書き換え法(仮称)」という方法がある。

  例題：
変数a,b,cに適当な数が代入されている。この変数中で最小値を変数wに代入する。

  考え方：
これはとりあえず１つめの変数を仮の最小値とする。 次の変数を仮の最小値と比較して小さければ、その値を仮の最小値にする。 これを変数の数だけ繰り返す。

import  java.io.*;

class Test{
    public static void main(String args[]){
        int a, b, c, w;
        a = 100;
        b = 200;
        c = 300;

        w = a;

        if(b < w)
            w = b;

        if(c< w)
            w = c;

        System.out.println("最小値は" + w + "です");
    }
}

  変数が多くなればこの方法でも、どんどんプログラムが長くなります。 もちろん実際のプログラムでは、どんどん長く書くわけではありません。 チェックする部分は同じような処理をさせているわけですので、後で述べる 反復構造を利用するのです。


2章：制御文

4. ケース構造

    ３つ以上の異なった処理の中から、条件にあった処理を行うことを多方向分岐と 言い、次のような書き方をする。

例）　charaの内容が'A'だったらwに65を、'B'ならば66をそれ以外なら0を代入する。

switch(chara){
    case 'A' : w=65; break;
    case 'B' : w=66; break;
    default  : w=0 ; break;
}

    なお、プログラム表記中の「break;」を忘れ、下のようなプログラムにすると、 左のフローチャートになる。しかし、このようなフローチャートは悪い見本であり、 正式なフローチャートではない。(正しく動かないという意味ではなく、 このようなアルゴリズムがきれいなアルゴリズムではないという意味。)

switch(chara){
    case 'A' : w=65;
    case 'B' : w=66;
    default  : w=0 ;
}

    「switch」文の括弧の中は、どんな型でも良いわけではなくて、 「byte」型、「char」型、「short」型そして「int」型のみです。


2章：制御文

例題

    西暦を入力し、その年の干支を表示させなさい。ただし西暦0年は申年とする。
import java.io.*;

class Test{
    public static void main(String args[]){
        int year;

        System.out.print("西暦を入力してください : ");
        year = readNumber();		// 西暦入力

        if( year < 0 )
            year = -year;			// 正に直す

        switch( year % 12 ){		// 12で割ったあまり
        case 0 :
            System.out.println(year+"年は申年です");
            break;
        case 1 :
            System.out.println(year+"年は酉年です");
            break;
        case 2 :
            System.out.println(year+"年は戌年です");
            break;
        case 3 :
            System.out.println(year+"年は亥年です");
            break;
        case 4 :
            System.out.println(year+"年は子年です");
            break;
        case 5 :
            System.out.println(year+"年は丑年です");
            break;
        case 6 :
            System.out.println(year+"年は寅年です");
            break;
        case 7 :
            System.out.println(year+"年は卯年です");
            break;
        case 8 :
            System.out.println(year+"年は辰年です");
            break;
        case 9 :
            System.out.println(year+"年は巳年です");
            break;
        case 10 :
            System.out.println(year+"年は午年です");
            break;
        case 11 :
            System.out.println(year+"年は未年です");
            break;
        }
    }

    // キーボードから数字を入力するメソッド
    public static int readNumber(){
        byte b[] = new byte[100];

        try{
             System.in.read(b);
             return Integer.parseInt((new String(b)).trim());
        }catch(Exception e){
             return 0;
        }
    }
}


2章：制御文

5. 反復構造(while)

    ここで紹介するwhile文は、前判定型であり、繰り返し処理に入る前に 繰り返し条件をチェックする。したがって、条件によっては繰り返し処理を １度も実行しない場合もあり得る。

    1から100までの合計を求めてみる。
import java.io.*;

class Test{
    public static void main(String args[]){
        int n, t;

        n = 1;
        t = 0;

        while(n <= 100){
            t = t + n;
            n = n + 1;
        }
        System.out.println("1から100までの合計は"+t+"です");
    }
}
    while文ではwhileの後の括弧の中に条件式を記述し、その条件式が真になる間 処理が繰り返される。もちろん、複数の条件式を「&&」「||」でつなげることもできる。


2章：制御文

6. 反復構造(do)

    ここで紹介するdo～while文は、後判定型であり、繰り返し処理を行った後に 繰り返し条件をチェックする。したがって、どんな条件でも最低１回は繰り返し処理が 実行される。

    前ページ同様、1から100までの合計を求めてみる。
import java.io.*;

class Test{
    public static void main(String args[]){
        int n, t;

        n = 1;
        t = 0;

        do{
            t = t + n;
            n = n + 1;
        }while(n <= 100);
        System.out.println("1から100までの合計は"+t+"です");
    }
}
    while文ではwhileの後の括弧の中に条件式を記述し、その条件式が真になる間 処理が繰り返される。もちろん、複数の条件式を「&&」「||」でつなげることもできる。


2章：制御文

7. 反復構造(for)

    ここで紹介するfor文は、前判定型であり、繰り返し処理に入る前に 繰り返し条件をチェックする。したがって、条件によっては繰り返し処理を １度も実行しない場合もあり得る。

    for文は、while文に初期設定と、変化式を組み合わせたものです。これまでの 例と同様に1から100までの合計を求めてみる。while文を使用すると下記のようになる。
import java.io.*;

class Test{
    public static void main(String args[]){
        int n, t;

        n = 1;			// 初期設定
        t = 0;			// 初期設定

        while(n <= 100){
            t = t + n;
            n = n + 1;	// 変化式
        }
        System.out.println("1から100までの合計は"+t+"です");
    }
}

    上のイタリックの部分は、繰り返し処理に入るための初期設定をしている部分で、 アンダーラインの部分は、次回の繰り返し処理のための最初期化をする部分である。for文ではこれらを 一緒に組み込むことができる。実際には下記のようになる。
import java.io.*;

class Test{
    public static void main(String args[]){
        int n, t;

        for( n = 1, t = 0 ; n <= 100 ; n = n + 1){
            t = t + n;
        }
        System.out.println("1から100までの合計は"+t+"です");
    }
}

    すなわち、

    for ( 初期設定 ; 条件式 ; 変化式 )
        繰り返し処理
    となる。

    初期設定、変化式はカンマ「,」で区切って複数の式にすることもできるが、 あまり書かない。また、初期設定、変化式が必要ない場合は、必ずしも記述する必要はなく、 省略することもできる。しかしこの場合もセミコロン「;」は省略できない。その例を 下記に示す。
import java.io.*;

class Test{
    public static void main(String args[]){
        int n, t;

        n = 1;			// 初期設定
        t = 0;			// 初期設定

        for( ; n <= 100 ; n = n + 1){
            t = t + n;
        }
        System.out.println("1から100までの合計は"+t+"です");
    }
}

    繰り返す処理が１文の時は、中括弧を省略し、下記のように書くこともできる。
import java.io.*;

class Test{
    public static void main(String args[]){
        int n, t;

        n = 1;			// 初期設定
        t = 0;			// 初期設定

        for( ; n <= 100 ; n = n + 1)
            t = t + n;

        System.out.println("1から100までの合計は"+t+"です");
    }
}
    このように処理が１文の時に中括弧を省略できるのは「if」「else」文、「while」文も同様である。


2章：制御文

8. 補助制御文

    反復構造では同じ処理を繰り返しましたが、場合によっては条件式が真でも 反復構造から抜け出したい場合が生じたりする場合もあります。このような 処理の流れはアルゴリズム的にはあまり良いことではないのですが、実際の プログラムでは、このように反復処理中にその処理から抜け出した方が、 プログラムがすっきり書ける場合も少なくありません。

    そのような時には補助制御文を使用します。補助制御文には以下のような物が 用意されています。

break
    「switch～case文」のところでも紹介しましたが、「while」「for」などの反復処理中に 使用すると、強制的に反復処理を抜けます。

    反復処理が入れ子になっている場合(多重反復処理)は、内側の反復構造を１つだけ 抜けるだけです。

continue
    反復構造中に使用すると、反復処理が終了したことになります。すなわち条件評価を 行い、条件が真ならばもう一度反復処理を実行します。なお、「for」文であれば、 変化式も実行した後に条件式を評価します。

goto
    ラベルへの無条件分岐です。C言語ではよっぽどのことがない限り使用しません。 この文が使われているプログラムを見たら、「よっぽどのことがあるのか、賢くない 人が作ったのかどちらかなんだ」と想ってください。

return
    その関数から抜けます。詳細は後述します。
    上で紹介した補助制御文のフローチャートを示します。
    以下に例を示します。この例では、数字を２つ入力しa,bに代入します。 そしてaからbまでの偶数の和を求めますが、bが100以上の場合はaから100までの偶数の 合計を求めます。なおaの方が大きい場合のチェックはしていません。
import java.io.*;

class Test{
    public static void main(String args[]){
        int a, b, t;

        a = readNumber();
        b = readNumber();

        for( t = 0 ; a <= b ; a++ ){
            if( a > 100 )		// 100以上になったら終了
                break;
            if( a % 2 == 1 )	// 偶数じゃなかったら、変化式を実行
                continue;
            t = t + a;
        }
        System.out.println(t);
    }

    // キーボードから数字を入力するメソッド
    public static int readNumber(){
        byte b[] = new byte[100];

        try{
             System.in.read(b);
             return Integer.parseInt((new String(b)).trim());
        }catch(Exception e){
             return 0;
        }
    }
}


2章：制御文

トップ-> Java入門：2章 制御文-> 補助制御文

← 前ページへ　:　 解答へ　:　 次ページへ →
9. 練習問題

問題
    キーボードから月(1～12)を入力し、その月の日数を表示するプログラムを作れ。

← 前ページへ　:　 解答へ　:　 次ページへ →


2章：制御文

9. 練習問題

問題
    キーボードから月(1～12)を入力し、その月の日数を表示するプログラムを作れ。

解答
import java.io.*;

class Test{
    public static void main(String args[]){
        int month;
        System.out.print("月(1～12)を入力してください : ");
        month = readNumber();

        switch(month){
        case 1:
        case 3:
        case 5:
        case 7:
        case 8:
        case 10:
        case 12:
            System.out.println(month+"月は31日です");
            break;
        case 2:
            System.out.println(month+"月は28日か29日です");
            break;
        case 4:
        case 6:
        case 9:
        case 11:
            System.out.println(month+"月は30日です");
            break;
        default:
            System.out.println("1から12を入力してください");
        }
    }

    // キーボードから数字を入力するメソッド
    public static int readNumber(){
        byte b[] = new byte[100];

        try{
             System.in.read(b);
             return Integer.parseInt((new String(b)).trim());
        }catch(Exception e){
             return 0;
        }
    }
}


2章：制御文

トップ-> Java入門：2章 制御文-> 補助制御文

← 前ページへ　:　 解答へ　:　 次章へ →
10. 練習問題

問題
    数当てゲーム。ある数を設定して、それを当てるゲームを作る。正解よりも小さい 値を入力したら「小さい」と、大きい値を入力したら「大きい」と表示する。 入力する範囲は0から9999に限定し、それ以外が入力されたらもう一度入力させる。 10回行っても正解しない場合は終了する。

← 前ページへ　:　 解答へ　:　 次章へ →


2章：制御文

10. 練習問題

問題
    数当てゲーム。ある数を設定して、それを当てるゲームを作る。正解よりも小さい 値を入力したら「小さい」と、大きい値を入力したら「大きい」と表示する。 入力する範囲は0から9999に限定し、それ以外が入力されたらもう一度入力させる。 10回行っても正解しない場合は終了する。
解答
import java.io.*;

class Test{
    public static void main(String args[]){
        int seikai = 1484;
        int count;
        int inp = -1;

        for( count = 1 ; count <= 10 ; count++ ){
            inp = -1;
            while( inp < 0 || inp > 9999 )
                inp = readNumber();

            if( inp == seikai ){
                System.out.println(count + "回目で正解ですね");
                break;
            }
            if( inp < seikai )
                System.out.println("小さい");
            else
                System.out.println("大きい");
        }
        if( inp != seikai )
            System.out.println("10回で正解しませんでした。ゲームオーバー");
    }

    // キーボードから数字を入力するメソッド
    public static int readNumber(){
        byte b[] = new byte[100];

        try{
             System.in.read(b);
             return Integer.parseInt((new String(b)).trim());
        }catch(Exception e){
             return 0;
        }
    }
}


3章：配列

1. 配列とは

    例えば100個のデータがあった場合、これを入れるための変数として、a1,a2,a3,・・・,a100と 変数を設定したのでは大変なことになってしまうことは明らかです。このデータの形式が 同じ場合、メモリー上に100個の連続した領域を確保し、何件ものデータを総括的に扱う ことができる方法を配列といいます。配列は、変数名と何番目のデータを指すかという 添え字で構成されています。

    例えば、１ヶ月のお店の来客数を管理するのに、raikyaku01,raikyaku02, raikyaku03,・・・raikya31なんてやっていたら大変なことになります。 そこでraikyakuという配列を日にちという添え字を用いて利用するわけです。具体的には raikyaku(1),raikyaku(2),・・・raikyaku(31)ってな具合です。

    配列を利用すると何が便利なのでしょうか？　それは添え字に変数が使えることです。 先ほどの１ヶ月の来客数を表示する場合、配列を使わないとraikyaku01, raikyaku02,raikyaku03,・・・raikya31と表示させたい変数を すべて列挙しなくてはなりません。配列を使うと、反復構造を使って、添え字の変数を 1ずつ増やしていくだけでいいのです。

    ところで、配列の添え字は0から始まるプログラム言語と、 1から始まるプログラム言語があります。。 JavaやC言語では0から始まります。これに対してCOBOLでは1から始まります。

    Javaでは、配列を入れるための変数を宣言し、配列の領域を確保し、確保した領域を 宣言した変数に入れると言う操作をしなくてはなりません。領域を確保する際には、 どのくらいの大きさの配列か(何番目までデータを入れるか)を指定しなければなりませんが、 配列を入れるための変数を宣言する際には、どのくらいの大きさかは宣言しなくても構いません。 この辺が、C言語の配列と少し違うところです。


3章：配列

2. 配列の宣言と初期化

    Javaでは、(1)配列を入れるための変数を宣言する手順と(2)配列の領域を確保し、先程の変数に代入する操作の ２つの手順が必要なことは、先程説明しました。
int raikyaku[];
  配列を入れるための変数の宣言
raikyaku = new int[30]
  配列の領域を確保し、変数に代入

    Javaではraikyaku[10]のように添え字を大括弧で表現しますが、フローチャート ではraikyaku(10)のように普通の括弧で表現します。

    また、Javaでは配列を作るとその中身はきれいにしてくれます。例えば、int型の配列を作成した 直後はその配列要素すべて０になっていますし、double型の配列を作成した場合も、その要素は 0.0で初期化されています。

import  java.io.*;

class Test{
    public static void main(String args[]){
        int a[];
        a = new int [10];

        System.out.println(a[2]);
    }
}

C:\java>javac Test.java

C:\java>java Test
0

C:\java>

    Javaでは、配列を使用する場合に配列の最後を越えてアクセスしていないかどうかをチェックしています。 つまり10個の要素しかない配列の11番目にアクセスすることはできません。(これを例外をスローすると言います。 例外については後述します)

import  java.io.*;

class Test{
    public static void main(String args[]){
        int a[];
        a = new int [10];

        // 要素数が10の配列の11番目にアクセスしようとした
        System.out.println(a[11]);
    }
}

C:\java>javac Test.java				←コンパイル正常に行われる

C:\java>java Test
java.lang.ArrayIndexOutOfBoundsException: 11	←実行時にエラーが出る

C:\java>

    配列の領域を確保するのと同時に、初期化を行うこともできます。

int raikyaku[] = { 1,2,3,4,5 };
    この場合、配列の大きさは、初期化の際の要素数になります。つまり上記の例では要素数が５つの配列になります。

    実際の配列領域を入れるための変数があると言うことは、配列領域を別の変数に代入することもできます。

import  java.io.*;

class Test{
    public static void main(String args[]){
        int a[] = { 1,2,3,4,5 };
        int b[];

        b = a;

        System.out.println(b[3]);

        a[3] = 100;

        System.out.println(b[3]);
    }
}

C:\java>javac Test.java

C:\java>java Test
4
100

C:\java>
    上の例では、要素数５つの配列をaという変数とbという変数の２つで保持しています。言い換えると、 aとbは同じ配列領域を指していることになります。したがって、aの配列を書き換えると、bも同じ配列を 指しているので、b[3]も100になるわけです。

    Javaでは変数を用いて配列の領域確保を行うことができます。すなわち動的に配列のサイズを変化させる ことができるのです。

import  java.io.*;

class Test{
    public static void main(String args[]){
        int i = 200;
        int a[] = new int[i];
    }
}


3章：配列

3. 配列の使用法

    配列は添え字を用いることで、その要素の読み書きをします。添え字は変数であっても 構いません。

    「int型」の要素数10個の配列を作り、これに1から10の数字を代入する例を示します。

// 添え字に変数を使わない例
import  java.io.*;

class Test{
    public static void main(String args[]){
        int a[] = new int[10];

        a[0] = 1;
        a[1] = 2;
        a[2] = 3;
        a[3] = 4;
        a[4] = 5;
        a[5] = 6;
        a[6] = 7;
        a[7] = 8;
        a[8] = 9;
        a[9] = 10;
    }
}

// 添え字に変数を使った例
import  java.io.*;

class Test{
    public static void main(String args[]){
        int a[] = new int[10];
        int i;	// 添え字

        for( i = 0 ; i < 10 ; i++ )
            a[i] = i+1;
    }
}
    このように、配列を使うことと、その添え字に変数を使うこと、反復処理を用いることで、 非常に便利になります。

    もし、上のプログラムを以下のように書き換えるとどうなるでしょう？

// 悪い例
import  java.io.*;

class Test{
    public static void main(String args[]){
        int a[] = new int[10];
        int i;	// 添え字

        for( i = 0 ; i <= 10 ; i++ )
            a[i] = i+1;
    }
}
    変更点は「for文」の条件式です。「=」を加えたことによって、 繰り返し処理の部分は11回実行されます。すなわち「a[10] = i + 1」が 実行されることになり、確保した配列の大きさを越えてしまっています。 つまり、実行時に何が起こるかわかりません。このようなミスは最初の頃は よくやるミスですので、気をつけてください。

    CやC++にはなかった機能として、配列の要素数を調べることができるようになりました。

import  java.io.*;

class Test{
    public static void main(String args[]){
        int a[] = new int[10];
        int i;	// 添え字

        for( i = 0 ; i < a.length ; i++ )
            a[i] = i+1;
    }
}
    上の例のアンダーラインのように、[配列の変数名.length]とすることで、配列の大きさを 取得できます。

配列のコピー

import  java.io.*;

class Test{
    public static void main(String args[]){
        int a[] = { 5,4,3,2,1 };
        int b[] = { 1,2,3,4,5 };

        // 配列の表示
        showArray(a);
        showArray(b);

        // 配列のコピー
        // 配列aの0番目からa.length個(すべて)を配列bの0番目へとコピーする
        System.arraycopy(a, 0, b, 0, a.length);

        // 配列の表示
        showArray(a);
        showArray(b);
    }

    // 配列を表示するメソッド
    public static void showArray( int a[] ){
        for( int i = 0 ; i < a.length ; i++ )
            System.out.print(i+":"+a[i]+"\t");
        System.out.println("");
    }
}

C:\java>javac Test.java

C:\java>java Test
0:5     1:4     2:3     3:2     4:1	←コピー前のa
0:1     1:2     2:3     3:4     4:5	←コピー前のb
0:5     1:4     2:3     3:2     4:1	←コピー後のa
0:5     1:4     2:3     3:2     4:1	←コピー後のb(コピーされている)

C:\java>
    上記のように、「System.arraycopy」メソッドを用いると、配列のコピーが簡単に行える。


3章：配列

トップ-> Java入門：3章 配列-> 練習問題

← 前ページへ　:　 解答へ　:　 次ページへ →
4. 練習問題

問題
    要素数10のchar型の配列を作り、「A」から「J」で初期化する。キーボードから 数字(0～9)を入力し、該当する文字を表示するプログラムを作れ。例えば、0を入力したら 「A」という文字を表示する。

← 前ページへ　:　 解答へ　:　 次ページへ →


3章：配列

4. 練習問題

問題
    要素数10のchar型の配列を作り、「A」から「J」で初期化する。キーボードから 数字(0～9)を入力し、該当する文字を表示するプログラムを作れ。例えば、0を入力したら 「A」という文字を表示する。

解答
import java.io.*;

class Test{
    public static void main(String args[]){
        int  i;
        char a[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'};

        System.out.print("0から9までの数字を入力してください  : ");
        i = readNumber();

        if( 0 <=i && i <= 9 )
            System.out.println(a[i]);
    }
    // キーボードから数字を入力するメソッド
    public static int readNumber(){
        byte b[] = new byte[100];

        try{
             System.in.read(b);
             return Integer.parseInt((new String(b)).trim());
        }catch(Exception e){
             return 0;
        }
    }
}


3章：配列

5. ２次元配列と多次元配列

    2次元配列とは、配列の配列です。イメージ的には表だと想ってください。１次元配列と同様に、 配列を入れるための変数を宣言し、配列領域を確保して変数に代入するという手続きが必要になります。
    int a[][];		// ２次元配列を入れるための変数
    a = new int[3][4];	// 3行4列の２次元配列領域を確保

    a[0][0] = 10;
    a[0][1] = 20;
    a[1][2] = 30;
    a[2][3] = 40;

    int a[][] = new int[3][4];	// 変数宣言と領域確保を同時にしてもよい

    a[0][0] = 10;
    a[0][1] = 20;
    a[1][2] = 30;
    a[2][3] = 40;
    上記の例の結果は以下のような状態になります。「?」は初期値(int型の場合は0)になります。
10 20 ? ?
? ? 30 ?
? ? ? 40
    下記のようにすると初期化できます。
   int  a[][] = { { 0, 1, 2, 3 }, { 4, 5, 6, 7 }, { 8, 9, 10, 11 }  };
   int  b[][] = { { 0          }, { 4, 5,      }, { 8, 9, 10, 11 }  };
   int  c[][] = { { 0, 1, 2, 3 }, { 4, 5, 6, 7 }, { }  };
    これらの結果は以下のようになります。
配列a(3行4列)
0 | 1 | 2 | 3
4 | 5 | 6 | 7
8 | 9 | 10 | 11
配列b(3行)
0 × × ×
4 5 × ×
8 9 10 11
配列c(3行)
0 | 1 | 2 | 3
4 | 5 | 6 | 7
× × × ×
×・・・存在しない領域
    ２次元配列では、初期化子の数だけ行・列が作られます(配列a参照)。ところが配列bの場合、３行目の 列は４つ初期化子があるにもかかわらず、１行目の列の初期化子は１つしかありません。この場合、領域が 確保されません。つまり１行目の列の要素数は１つしかないのです。CやC++では一番大きい列に合わせられ、 強制的に３行４列の長方形の２次元配列になりましたが、Javaでは各行が１次元配列を持つと言う考え方に なっていますので、長方形にはなりませんので注意が必要です。つまり、１次元配列を１次元配列で管理し ているのです。

import java.io.*;

class Test{
    public static void main(String args[]){
        int  a[][] = { { 0, 1, 2, 3 }, { 4, 5, 6, 7 }, { 8, 9, 10, 11 }  };
        int  b[][] = { { 0          }, { 4, 5,      }, { 8, 9, 10, 11 }  };
        int  c[][] = { { 0, 1, 2, 3 }, { 4, 5, 6, 7 }, { }  };

        System.out.println("aの行"+a.length);
        System.out.println("a[0]の列"+a[0].length);
        System.out.println("a[1]の列"+a[1].length);
        System.out.println("a[2]の列"+a[2].length);

        System.out.println("bの行"+b.length);
        System.out.println("b[0]の列"+b[0].length);
        System.out.println("b[1]の列"+b[1].length);
        System.out.println("b[2]の列"+b[2].length);

        System.out.println("cの行"+c.length);
        System.out.println("c[0]の列"+c[0].length);
        System.out.println("c[1]の列"+c[1].length);
        System.out.println("c[2]の列"+c[2].length);

        System.out.println("b[0][0]"+b[0][0]);
        System.out.println("b[0][2]"+b[0][2]);	// 実行時に例外(エラー)が出てしまう
    }
}

C:\java>javac Test.java

C:\java>java Test
aの行3
a[0]の列4
a[1]の列4
a[2]の列4
bの行3
b[0]の列1
b[1]の列2
b[2]の列4
cの行3
c[0]の列4
c[1]の列4
c[2]の列0
b[0][0]0
java.lang.ArrayIndexOutOfBoundsException: 2
        at Test.main(Test.java:25)

C:\java>

    また、２次元配列は１次元配列を１次元配列で管理しているということは、１次元配列を入れるための １次元配列のみ(すなわち行)を確保し、後で列を確保することもできます。
import java.io.*;

class Test{
    public static void main(String args[]){
        // 列を管理するための行のみを確保する
        int  d[][]  = new int[3][];

        // 0行目の列は要素数５
        d[0] = new int[5];

        // 1行目の列は要素数４
        d[1] = new int[4];

        // 2行目の列は要素数３
        d[2] = new int[3];

        System.out.println("dの行"+d.length);
        System.out.println("d[0]の列"+d[0].length);
        System.out.println("d[1]の列"+d[1].length);
        System.out.println("d[2]の列"+d[2].length);
    }
}

C:\java>javac Test.java

C:\java>java Test
dの行3
d[0]の列5
d[1]の列4
d[2]の列3

C:\java>
配列のイメージ
0 0 0 0 0 0
0 0 0 0 × ×
0 0 0 × × ×
×・・・領域が確保されていない

多次元配列

    3次元配列以上の物を多次元配列と言います。原理的には何次元でも作ることができます。 例えば、３次元配列を作る場合には下記のようにします。
   int  a[10][10][10];
    しかし、例えばint型、1024×1024×1024の３次元配列を考えると、4GBytesという 膨大なメモリーを消費することになります。


3章：配列

6. 練習問題

問題
    4行20列の配列を作る。キーボードから名前を４人分入力してその配列に格納する。 そして確認のために、その４人分の名前をディスプレイに表示するプログラムを作れ。

    ただし、文字列を読んでbyte配列として返すメソッドは以下のものを使用する。また、 byte配列を表示するためには以下のようにする。

// 名前を読み込んで、byte配列として返す
public static byte[] readName(){
    byte b[] = new byte[20];
    try{
        System.in.read(b);
    }catch(Exception e){}

    return (new String(b)).trim().getBytes();
}

// byte配列の表示の仕方
byte b[] = {'A', 'B', 'C', '\0'};	// bという配列に名前が入っているとする

System.out.print( new String(b) );


3章：配列

6. 練習問題

問題
    4行20列の配列を作る。キーボードから名前を４人分入力してその配列に格納する。 そして確認のために、その４人分の名前をディスプレイに表示するプログラムを作れ。

    ただし、文字列を読んでbyte配列として返すメソッドは以下のものを使用する。また、 byte配列を表示するためには以下のようにする。

// 名前を読み込んで、byte配列として返す
public static byte[] readName(){
    byte b[] = new byte[20];
    try{
        System.in.read(b);
    }catch(Exception e){}

    return (new String(b)).trim().getBytes();
}

// byte配列の表示の仕方
byte b[] = {'A', 'B', 'C', '\0'};	// bという配列に名前が入っているとする

System.out.print( new String(b) );

解答
import  java.io.*;

class Test{
    public static void main(String args[]){
        byte meibo[][] = new byte[4][];
        int  i;

        // 名前入力
        for( i = 0 ; i < 4 ; i = i + 1 ){
            System.out.print((i+1)+"人目の名前を入力してください : ");
            meibo[i] = readName();
        }

        //  確認のための表示
        for( i = 0 ; i < 4 ; i = i + 1 )
            System.out.println((i+1) + "人目は [" + new String(meibo[i]) + "] さんです");
    }

    // 名前を読み込んで、byte配列として返す
    public static byte[] readName(){
        byte b[] = new byte[20];
        try{
            System.in.read(b);
        }catch(Exception e){}

        return (new String(b)).trim().getBytes();
    }
}

1人目の名前を入力してください : 宇多田ヒカリ
2人目の名前を入力してください : 倉木真衣
3人目の名前を入力してください : 阿部なつみ
4人目の名前を入力してください : 鈴樹あみ

1人目は [宇多田ヒカリ] さんです
2人目は [倉木真衣] さんです
3人目は [阿部なつみ] さんです
4人目は [鈴樹あみ] さんです


4章：クラスの基礎

1. クラスとは

    クラスとは、前述しましたように、プログラムをある実世界のモノと考え、 それに必要なデータと働き(メソッド)をひとまとめにしたものです。

    これまでに出てきたクラスは、「System」クラスと「String」クラスがあります。Systemクラスは ディスプレイに文字を表示するための標準出力、標準エラー出力に表示させるために用いてきました。 また、解説はしませんでしたが、キーボードから文字を入力するときの標準入力を使用しました。さらに、 配列をコピーするのに、Systemクラスのarraycopyメソッドを使用しました。このようにSystemクラスは その名の通り、システムに関係する操作などをになうクラスと言えます。

    Stringクラスはまだ詳しく解説していませんが、文字列を担うクラスです。そのデータ(クラス変数) として文字列そのものを持ち、メソッドとして検索したり、つなげたり、部分文字列を抽出したりといった メソッドが定義されています。

    このように、データとそれらを扱うメソッドを作成したクラスは、そのクラスの内部がどのように なっているのかを知らなくても、部品のように使用することができます。つまり、SystemクラスやStringクラスの 中身を知らなくても(ソースコードを見なくても)、そのクラスを使って、プログラムを作ることができます。 さらに、その部品 (クラス)は他のプログラムを作成するときにも使い回しができるのです。

    なお、クラスのデータのことを「メンバー変数」、そのデータを扱うメソッドを「メンバーメソッド」 と言い、両者を併せて「メンバー」と言います。後で詳しく述べますが、ただ単純にメンバー変数に代入 したり、メンバー変数を取得するためだけのメソッドを「アクセサメソッド」と言います。

    はじめてクラスと言う言葉を耳にした人はよくわからないと思いますので、実際にくらすを作ってみましょう。


4章：クラスの基礎

2. クラスの作成

    今まで、Testクラスしか作成しませんでしたが、ここではTestクラスのほかにMyPointクラスというものを 作ってみます。MyPointクラスは座標を担うクラスで、メンバー変数として、X座標とY座標を持ちます。 メソッドとしては、２点間の座標を求めるメソッドと、ディスプレイに表示するためにString型に変換する メソッドを持ちます。

    クラスを作成する場合、前述しましたが基本的に１ファイルに１クラスで、ファイル名はクラス名と同じに する必要があります。注意してください。

MyPoint.java

class MyPoint{
    double  m_x;
    double  m_y;

    // 文字列に変換する
    public String toString(){
        return "X:"+m_x+"  Y:"+m_y;
    }

    // 距離を計算する
    public double calcDistance(MyPoint pnt){
        // Mathクラスは数学関係を担うクラス
        // Math.pow累乗を計算するメソッド
        // Math.sqrtはルートを計算するメソッド
        return Math.sqrt(Math.pow((m_x - pnt.m_x), 2) + Math.pow((m_y - pnt.m_y), 2));
    }
}

Test.java

import  java.io.*;

class Test{
    public static void main(String args[]){
        int n;
        MyPoint pnt1 =  new MyPoint();	// (1)
        MyPoint pnt2 =  new MyPoint();

        pnt1.m_x = 10;
        pnt1.m_y = 20;

        pnt2.m_x = 50;
        pnt2.m_y = -20;

        System.out.println("pnt1 " + pnt1.toString());
        System.out.println("pnt2 " + pnt2.toString());
        System.out.println("距離:" + pnt1.calcDistance(pnt2));
    }
}

C:\java>javac Test.java

C:\java>java Test
pnt1 X:10.0  Y:20.0
pnt2 X:50.0  Y:-20.0
距離:56.568542494923804

C:\java>
    クラスを定義した(MyPoint.javaを作成しただけ)では、クラスを使えるようにはなりません。 Test.javaの(1)と書いてあるように「new」をして初めてクラスを使えるようになります。この newをインスタンスを作るまたはオブジェクトを作ると言います。

    C言語の構造体やC++のクラスのように以下のように記述しただけでは、インスタンスを入れる 変数を用意しただけで、インスタンスは作られません。つまり、CやC++のポインタと同じような 感じです。

  MyPoint pnt;

    インスタンスの変数やメソッドを使う場合は、「インスタンス名.変数名」または 「インスタンス名.メソッド名」で使用します。

    C++を知っている人は気持ち悪いかもしれませんが、newをしたらdeleteしなければならないのが C++ですが、Javaではdeleteはありません。JVMが「もう必要ないだろうなぁ」と思ったところで、 勝手にdeleteしてくれます。(配列も同様です)

    newを何個も行うことで、インスタンスをたくさん作ることができます。これらのインスタンスは 別々のものですので、変数の値もインスタンスが異なれば違う値を保持します。この場合、pnt1の メンバー変数のm_xとpnt2のm_xは違う値を保持しているのはこのためです。


4章：クラスの基礎

3. コンストラクタ

    クラスには、インスタンスが作られた時に１度だけ実行される「コンストラクタ」というものがある。 一般にコンストラクタにはそのクラスインスタンスを初期化するために使用される。

    例えば、先程のMyPointクラスはインスタンス作成時にX座標、Y座標とも0に初期化されていたが (メンバー変数は0で初期化される)、これをX:10, Y:20で初期化したいというような場合に使う。

    コンストラクタの名前はクラス名と同じ名前で、引数をとることはできるが、戻り値はない。 戻り値がないのは決まっている事なので、戻り値(void)は記述しない。また、コンストラクタに 限ったことではないが、引数が違えば同じ名前で複数のコンストラクタや メソッドを作ることができる。これに関しては後述する。

MyPoint.java

class MyPoint{
    double  m_x;
    double  m_y;

    // コンストラクタ
    public MyPoint(){
        System.out.println("コンストラクタ１が呼ばれました");
        m_x = 10;
        m_y = 20;
    }

    // コンストラクタ2
    public MyPoint(double x, double y ){
        System.out.println("コンストラクタ２が呼ばれました");
        m_x = x;
        m_y = y;
    }

    // 文字列に変換する
    public String toString(){
        return "X:"+m_x+"  Y:"+m_y;
    }

    // 距離を計算する
    public double calcDistance(MyPoint pnt){
        // Mathクラスは数学関係を担うクラス
        // Math.pow累乗を計算するメソッド
        // Math.sqrtはルートを計算するメソッド
        return Math.sqrt(Math.pow((m_x - pnt.m_x), 2) + Math.pow((m_y - pnt.m_y), 2));
    }
}

Test.java

import  java.io.*;

class Test{
    public static void main(String args[]){
        int n;
        MyPoint pnt1 =  new MyPoint();
        MyPoint pnt2 =  new MyPoint(50, -20);

//      pnt1.m_x = 10;
//      pnt1.m_y = 20;

//      pnt2.m_x = 50;
//      pnt2.m_y = -20;

        System.out.println("pnt1 " + pnt1.toString());
        System.out.println("pnt2 " + pnt2.toString());
        System.out.println("距離:" + pnt1.calcDistance(pnt2));
    }
}

C:\java>javac Test.java

C:\java>java Test
コンストラクタ１が呼ばれました
コンストラクタ２が呼ばれました
pnt1 X:10.0  Y:20.0
pnt2 X:50.0  Y:-20.0
距離:56.568542494923804

C:\java>


4章：クラスの基礎

6. 例題）スタッククラス

    例題として、逆ポーランド電卓を作ります。

    逆ポーランド電卓とは、「1 2 +」は「3」、「15 10 -」は「5」、「15 10 - 3 *」は「15」 というような感じで、数字が入力されたらスタックにプッシュし、+のような演算子が来たら スタックから数字を２つポップして計算結果をスタックにプッシュするような計算機です。

    ※ただし、javaで引数を「*」と入力すると、ディレクトリないにあるファイル名と解釈されるため、 かけ算の「*」は「@」で代用することにする。

    逆ポーランド電卓を作るためにはスタックを作る必要があります。

    コンストラクタにスタックサイズを指定します。デフォルトは100です。デフォルトでは int 100個分の配列をnewします。また、pushで値をこの配列に格納します。popでは、pushした値を 配列から取り出して返します。getLengthでは現在のスタックに溜められている個数を返します。peekでは popと同じですが、スタックの位置を変更しないよう、スタック要素を覗き見るようにします。

    メンバー変数は、配列を格納するint型ポインタと、現在のスタック位置を示す スタックポインタを持ちます。

    なお、コマンドラインからの引数は、mainメソッドのStringの配列argsに配列で渡されます。 すなわちコマンドラインの引数が、半角スペースが区切りになって分けられ、配列になります。 個数は、配列.lengthで求めることができます。

    以上をクラスにすると以下のようになるはずです。
IntStack.java
public class IntStack{
    ////////////////////////////////////////////////////
    // 管理情報

    int m_sp;	// スタックポインタ：次にプッシュする位置
    int m_size;	// スタックサイズ
    int m_stack[];	// スタック

    // コンストラクタ：引数あり
    // IntStack オブジェクトが定義された時に自動的に呼び出される
    public IntStack( int sz ){
        // スタックサイズを保存する
        m_size = sz ;

        // スタックの実体(size個のint配列)を自由記憶上に割り当てる
        m_stack = new int[m_size] ;

        // スタックポインタを初期化する
        m_sp = 0 ;
    }

    // コンストラクタ：引数なし
    // IntStack オブジェクトが定義された時に自動的に呼び出される
    public IntStack( ){
        // スタックサイズを保存する
        m_size = 100;

        // スタックの実体(size個のint配列)を自由記憶上に割り当てる
        m_stack = new int[m_size];

        // スタックポインタを初期化する
        m_sp = 0;
    }

    // プッシュ
    public void push( int value ){
        if( m_sp >= m_size ){
            System.err.println("stack overflow");
            System.exit(1) ;        // 異常終了する
        }

        m_stack[m_sp] = value ;
        m_sp = m_sp + 1 ;
    }

    // ポップ
    public int pop(){
        // アンダーフローのチェック
        if( m_sp <= 0 ){
            System.err.println("stack underflow");
            System.exit(1) ;        // 異常終了する
        }

        m_sp = m_sp - 1;
        return m_stack[m_sp];
    }

    // 現在のスタック長を獲得する
    public int getLength(){
        return m_sp;
    }

    // 指定位置(スタックトップからのオフセット)のスタック要素を覗き見る
    public int peek(){
        // オフセットのチェック
        if( 0 >= m_sp ){
            System.err.println("stack underflow");
            System.exit(1) ;        // 異常終了する
        }

        // 指定位置の要素をコピーする
        return m_stack[m_sp] - 1;
    }
}
Calc.java
import  java.io.*;

class Calc{
    public static void main(String args[]){
        int value, operand1, operand2 ;

        // スタックテンプレートクラスから、実数スタックを具体化する
        IntStack ostack = new IntStack( args.length );

        // コマンド行引数からリバーシュポーリッシュ記法で記述された式を
        // 読み込み計算する
        for( int i = 0 ; i < args.length ; i = i + 1 ){
            // 加法
            if( args[i].equals("+") ){
                operand2 = ostack.pop();
                operand1 = ostack.pop();
                ostack.push( operand1 + operand2 );
            }

            // 減法
            else if( args[i].equals("-") ){
                operand2 = ostack.pop();
                operand1 = ostack.pop();
                ostack.push( operand1 - operand2 );
            }

            // 乗法
            else if( args[i].equals("@") ){
                operand2 = ostack.pop();
                operand1 = ostack.pop();
                ostack.push( operand1 * operand2 );
            }

            // 除法
            else if( args[i].equals("/") ){
                operand2 = ostack.pop();
                operand1 = ostack.pop();

                // 0除算のチェック
                if( operand2 == 0 ){
                    System.err.println("divid by 0");
                    System.exit(1);	// 異常終了する
                }

                ostack.push( operand1 / operand2 );
            }

            // 上記以外は整数を仮定する
            else{
                value = (new Integer(args[i])).intValue();
                ostack.push( value );
            }
        }

        // スタックの整合性の確認
        if( ostack.getLength( ) != 1 ){
            System.err.println("illegal expression");
            System.exit(1);	// 異常終了する
        }

        // 答を出力する
        value = ostack.pop();
        System.out.println(value);

        // 正常終了
    }
}

C:\java>java clac 15 10 - 3 @
15

C:\java>
    文字列を比較するときは、「==」ではなく、Stringクラスの「equals」メソッドを使用します。 この理由については後述します。


5章：演算子

1. 演算子の種類

    １章では、簡単な演算について説明した。ここでは以下に示す 演算子について説明する。
  • 演算子
    • 算術演算子
      • 四則演算子
      • 符号変換
    • 比較演算子
      • 大小比較
      • 等価、非等価
    • 論理演算子
      • 論理否定
      • 論理積
      • 論理和
    • インクリメント演算子
    • デクリメント演算子
    • ビット演算子
      • 論理積
      • 論理和
      • 排他的論理和
      • 論理否定(1の補数)
    • シフト演算子
      • 左シフト
      • 右シフト
    • 代入演算子
      • 代入
      • 四則演算
      • シフト演算
      • ビット演算
    • cast演算子
    • 順次演算子
    • 条件演算子
    • instanceof演算子

+, -, *, /, %
-

<, >, <=, >=
==, !=

!
&&
||
++
--

&
|
^
~

<<
>>

=
+=, -=, *=, /=, %=
<<=, >>=, >>>=
&=, |=, ^=
(型)　例えば、(int), (char)など
,
? :
instanceof


5章：演算子

2. インクリメント演算子、デクリメント演算子

    int i;
    for( i = 0 ; i < 10 ; i = i + 1 )
        System.out.println(i);
    上記の例のように、今まで変数を１増やす場合「i = i + 1」と 書いてきた。しかし「変数を１増やす」と言う作業は頻繁に行われるため、 特別の演算子が用意されている。上の例は、その演算子を使用すると 下記のようになる

    int i;
    for( i = 0 ; i < 10 ; i++ )
        System.out.println(i);
    この「++」がインクリメント演算子である。 逆に「変数の値を１減らす」演算子はデクリメント演算子と言い、 「--」である。

    この演算子は、「i++」のように変数の後に書くことも、「++i」 のように変数の前に書くこともできます。しかし「j = i++」のように 式の中に組み込むと、変数の前に書いた場合と、変数の後に書いた場合の動作が 異なります。変数の前に書いた場合は、式を評価する前に インクリメントします。逆に 変数の後に書いた場合は、式を評価した後にインクリメントします。
import java.io.*;

class Test{
    public static void main(String args[]){
        int a, b;
        a = 3;
        b = ++a;

        System.out.println("a=" + a + "  b=" + b);
    }
}

import java.io.*;

class Test{
    public static void main(String args[]){
        int a, b;
        a = 3;
        b = a++;

        System.out.println("a=" + a + "  b=" + b);
    }
}

D:\java>javac Test.java

D:\java>java Test
a=4  b=4

D:\java>

D:\java>javac Test.java

D:\java>java Test
a=4  b=3

D:\java>
    ここで注意があります。１つの式の中に 同じ変数に対するインクリメント演算子が複数あってはならない ということです。もちろんデクリメント演算子も同じ注意があります。

    例えば、「b = a++ + a++」というのはダメです。予期しない結果になることが多く、 バグを招きかねません。「printf("%d %d\n", ++a, ++a);」という のもダメです。
import java.io.*;

class Test{
    public static void main(String args[]){
        int a, b;
        a = 0;
        b = ++a + ++a;

        System.out.println("a=" + a + "  b=" + b);
    }
}

import java.io.*;

class Test{
    public static void main(String args[]){
        int a, b;
        a = 0;
        b = a++ + a++;

        System.out.println("a=" + a + "  b=" + b);
    }
}

D:\java>javac Test.java

D:\java>java Test
a=2  b=3

D:\java>

D:\java>javac Test.java

D:\java>java Test
a=2  b=1

D:\java>
    「b = a+++ ++a;」なんてのもエラーにはなりませんが、他の人が見たときにわかりにくいし、 自分でもどんな結果になるかわかりにくいので、素直に複数行に分けて記述するべきです。ちなみに、a=0のとき、 a=2, b=2になります。まあ、なんとなくわからない結果でもないですけどもね・・・。


5章：演算子

3. 複合代入演算子

    複合代入演算子とは、演算と代入を一つの演算子にまとめたものです。 インクリメント演算子やデクリメント演算子も 複合代入演算子の一種と言うことができます。複合代入演算子には以下のような 種類があります。
複合代入演算子 使用例 意　味 普通の演算子での記述
+= a+=b; aにbを加えた値をaに代入する a=a+b
-= a-=b; aからbを引いた値をaに代入する a=a-b
*= a*=b; aとbを乗じた値をaに代入する a=a*b
/= a/=b; a÷bをaに代入する a=a/b
%= a%=b; a÷bの余りをaに代入する a=a%b
>>>= a>>>=b; aをbビット右にシフトした値をaに代入する(符号なし整数, ビット列) a=a>>b
>>= a>>=b; aをbビット右にシフトした値をaに代入する(符号付き整数) a=a>>b
<<= a<<=b; aをbビット左にシフトした値をaに代入する a=a<<b
&= a&=b; aとbの論理積をaに代入する a=a&b
|= a|=b; aとbの論理和をaに代入する a=a|b
^= a^=b; aとbの排他的論理和をaに代入する a=a^b


5章：演算子

4. ビット演算子

    ビット演算子とは、各変数をビット単位で処理(計算)するための演算子で、 以下のような種類かがあります。
演算子 意　味 使用法
& ビット積(AND) a=b&c; bとcの各ビット毎の論理積をとり、その結果をaに代入する
| ビット和(OR) a=b|c; bとcの各ビット毎の論理和をとり、その結果をaに代入する
^ ビット排他的論理和(XOR) a=b^c; bとcの各ビット毎の排他的論理和をとり、その結果をaに代入する
~ ビット否定(NOT) a=~b; bの各ビットを反転しその結果をaに代入する
AND

b | c | 結果(a)
0 | 0 | 0
0 | 1 | 0
1 | 0 | 0
1 | 1 | 1
    AND演算はビットを0でマスクするのに使用します。マスクとは、あるビットを 0もしくは1に変えることを言います。例えば、奇数か偶数かを調べるためには、 最下位を残して0でマスクします。その値が1なら奇数、0なら偶数と言うことになります。
import java.io.*;

class Test{
    public static void main(String args[]){
        int i = readNumber();

        if( i & 1 == 1 ){
            System.out.println("奇数です");
        }
        else{
            System.out.println("偶数です");
        }
    }

    // キーボードから数字を入力するメソッド
    public static int readNumber(){
        byte b[] = new byte[100];

        try{
             System.in.read(b);
             return Integer.parseInt((new String(b)).trim());
        }catch(Exception e){
             return 0;
        }
    }
}
OR

b | c | 結果(a)
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 1
    OR演算はビットを1でマスクするのに使用します。

XOR

b | c | 結果(a)
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0
    XOR演算はビットを必要な部分だけ反転させるのに使用します。

NOT

b | 結果(a)
0 | 1
1 | 0
    NOT演算は１の補数を表します。１の補数とは全てのビットを反転します。


5章：演算子

5. シフト演算子

    シフト演算とは論理演算と同じようにビットを操作する命令である。具体的には、 2進数の各ビットをずらすことである。右にずらすか左にずらすか、符号なしか、符号付きか によって３種類の演算子が 用意されている
演算子 意　味 使用法
<< 左にシフト a = b << c bを左にcビットずらす
>> 右にシフト a = b >> c bを右にcビットずらす(符号付き)
>>> 右にシフト a = b >> c bを右にcビットずらす(符号なし)
    一概にシフトするといっても、ずらすデータが符号付き整数か、符号なし整数かによって、 動作が異なる。符号なし整数と言っても、JavaではC言語と異なり、unsignedは存在しないので、 単なるビット列として扱いたい場合は、符号なし整数と考える。

    なお、floatやdouble、後述するポインタ型には使用できない。

    まず、符号なし整数(単なるビット列)のシフト命令について説明する。 これらは論理シフトと言い、ずらして足りなくなる１桁には無条件に0が入る(下図参照)。

    次に、符号付き整数のシフトを説明する。符号付き整数は、上位1bitが符号を表している。 この符号を壊さないようにシフトさせる(下図参照)。この理由は後述する。

    さて、シフトを行うとデータはどうなるのだろうか？まず、左にずらした時の例を示す。

    このように左にずらすと、その値は2倍になる。

    次に、右にずらすことを考える(下図参照)。

    右にずらす場合、符号付き整数(>>)と負号なし整数(>>>)でこのような差がでる。


5章：演算子

6. キャスト演算子

    キャストとは「型変換」のことです。下の例のように代入元と代入先の変数の型が 異なる場合、型変換を行う必要があります。一般に「byte」型から「int」型への変換など、 小さい入れ物から大きな入れ物へは自動的にキャストしてくれる。これを「暗黙の型変換」 という。
  int  i = 100;
  byte b = 100;

  i = b;		// 暗黙の型変換

    逆に大きな入れ物から小さな入れ物へのキャストは、データが失われる可能性があるため、 暗黙の型変換は行われずにエラーが出てしまう。このような場合、コンパイラに「データが失われる かもしれないのは、わかっているよ。」と教えなければならない。これを「明示的な型変換」と言う。
  int  i = 100;
  byte b = 100;

  b = i;		// エラー
  b = (byte)i;	// 明示的なキャスト

   また、整数と小数(実数)の関係も場合によっては明示的な型変換が必要になります。 下の左の例では明示的な型変換はしていません。右の例では明示的な型変換をしています。 キャストしなければわり算の結果は整数になってしまいます。
import java.io.*;

class Test{
    public static void main(String args[]){
        int   a = 300;
        float f = a / 7;

        System.out.println(f);
    }
}

import java.io.*;

class Test{
    public static void main(String args[]){
        int   a = 300;
        float f = (float)a / 7;

        System.out.println(f);
    }
}

C:\java>javac Test.java

C:\java>java Test
42.0

C:\java>

C:\java>javac Test.java

C:\java>java Test
42.857143

C:\java>


5章：演算子

7. 順次演算子

    カンマで区切られたいくつかの式を左から順に実行します。このカンマのことを 順次演算子と言います。下のfor文の中で使っているものがそうです。
import java.io.*;

class Test{
    public static void main(String args[]){
        int i;
        int j;

        for( i = 0, j = 10 ; i < 10 ; i++, j-- )
            System.out.println(i*j);
    }
}

C:\java>javac Test.java

C:\java>java Test
0
9
16
21
24
25
24
21
16
9

C:\java>

    順次演算子はC言語ではどこでも使えるが、Javaでは主にfor文の初期化と変化式のみで使用される。

    a=a+1, a=a*2, a=a+3;	// C言語ではOKだが、Javaではエラー

    下の例は、確かに左から右に実行されていることを証明する例です。左から実行されていれば５に、 右から実行されていれば７になりますが、結果は５が表示されています。
import java.io.*;

class Test{
    public static void main(String args[]){
        int a = 0;
        for( a=a+1, a=a*2, a=a+3 ; false ; );
        System.out.println(a);
    }
}

C:\java>javac Test.java

C:\java>java Test
5

C:\java>


5章：演算子

8. 条件演算子

    条件式が「真」の場合、「偽」の場合それぞれに実行される文が短い場合に用いられる演算子です。
演算子 意　味 使用法
? : 「if～else」文と同じ 式1 ? 式2 : 式3
式1が真なら式2が実行される。
式1が偽なら式3が実行される。
    したがって下の例は同じ働きをする。
    if( a > 10 )
          b = a;
    else
          b = -a;

    a > 10 ? (b = a) : (b = -a);

    また、この演算子を入れ子にすることもできる。しかしこの演算子の入れ子は、後でバグが出たときに見直したり、共同開発している 他のプログラマーが見ると、とても見にくいので、入れ子にすることはあまりおすすめしない。

    if( a > 10 ){
        if( a > 100 )
            b = 2;
        else
            b = 1;
    }
    else
        b = 0;

    a > 10 ? a > 100 ? (b = 2) : (b = 1) : (b = 0);

    // これも、上と同じ
    b = (a > 10 ? a > 100 ? 2 : 1 : 0);

    この演算子の結果は最後に実行された式の値になる。したがって右側の２行目の文では、 a=1000のとき実行されるのは「2」となり、演算子の結果も「2」となる。それが変数bに 代入されるので、最終的には「b = 2」となる。


5章：演算子

9. instanceof演算子

    instanceof演算子とは、そのクラスのインスタンスかどうかを調べるための演算子で、trueかfalseが 返されます。

Test.java

import java.io.*;

class A{
     int a;
}

class B{
     int a;
}

class Test{
    public static void main(String args[]){
        boolean bool;

        A a = new A();
        B b = new B();

        bool = a instanceof A;
        System.out.println(bool);

        if( b instanceof B )
             System.out.println("bはBです");
        else
             System.out.println("bはBではありません");

        // コンパイル時に絶対trueにならないとわかるので、コンパイルエラーになる
//      bool = a instanceof B;
    }
}

C:\java>javac Test.java

C:\java>java Test
true
bはBです

C:\java>
※この場合Test.javaには３つのクラスが記述されていますので、あまりよくないですが・・・。

    何の約に立つ演算子かここではわからないと思います。実は全てのクラスはObjectクラスと言うクラスの一種です。 正確には、全てのクラスはObjectクラスのサブクラスと言います。

    つまり、Objectクラスを入れるための変数に、AクラスのインスタンスやBクラスのインスタンスを入れることが できるのです。したがって、どっちを入れたかわからなくなってしまった場合に判断するのに必要な演算子です。 しかし、instanceof演算子はJava固有の演算子であるので、他の言語に移植する可能性が少しでもあるプログラムは この演算子を使わないように作る方が無難です。

Test.java

import java.io.*;
import java.util.Random;	// Randomクラスを使えるようにする

class A{
     int a;
}

class B{
     int a;
}

class Test{
    public static void main(String args[]){
        Object obj;

        // (new Random()).nextInt()でint型の乱数を生成する
        // つまり、乱数が奇数ならAクラスのインスタンスを、偶数ならBクラスのインスタンスを作る
        if( (new Random()).nextInt() % 2 == 1 )
            obj = new A();
        else
            obj = new B();

        System.out.println( "objは" + (obj instanceof A ? "A" : "B") + "クラスのインスタンスです" );
    }
}

C:\java>javac Test.java

C:\java>java Test
objはAクラスのインスタンスです

C:\java>java Test
objはBクラスのインスタンスです

C:\java>java Test
objはBクラスのインスタンスです

C:\java>
※この場合Test.javaには３つのクラスが記述されていますので、あまりよくないですが・・・。


5章：演算子

10. 演算子の優先順位

    今までに紹介した演算子には優先順位があります。例えば、足し算よりもかけ算の方が 優先順位が高いので、かけ算を先に行う等です。同じ優先順位のものは場合によっては 左にある演算子から右に、場合によっては逆に右から左に評価される場合もあります。 例えば、引き算は左にある物から評価されるので、普通の算数の場合と同じになります。

優先順位 | 演算子の種類 | 演算子
↑
高

低
↓ 		式 ()　[]　.※1
単項演算子 | !　~　++　--　(キャスト)
二項演算子 | instanceof | instanceof
剰余 *　/　%
加減 +　-
シフト <<　>>>　>>
比較 <　<=　> >=
等値、非等値 ==　!=
論理積(ビット演算子) &
排他的論理和(ビット演算子) ^
論理和(ビット演算子) |
論理積 &&
論理和 ||
条件演算子 ?　:
代入演算子、複合代入演算子 =　+=　-=　*=　/=　%=　<<=　>>>=　>>=　&=　^=　|=
順次演算子 ,
  ：　同順位の演算子が並んだ場合、左にある物から評価される
  ：　同順位の演算子が並んだ場合、右にある物から評価される

※1　：　クラスで使われる演算子

    これらの優先順位はすべて覚える必要はありません。どちらかわからなくなった場合は 括弧で囲めばいいのです。逆に括弧をつけないで書こうとすると、バグの原因になったり、 見直すときに見にくくなります。

    また、論理演算子の「&&」「||」では注意が必要になります。「&&」の場合、 １項目が「偽」になると２項目以降は評価されません。逆に「||」では１項目が「真」に なった時点で２項目以降は評価されません。

    int  a, b, c;

    if( (a==1) || (b==2) || (c==3) )
c==3が評価されるのは「a!=1」でかつ「b!=2」の時だけです。

    int  a, b, c;

    if( (a==1) && (b==2) && (c==3) )
c==3が評価されるのは「a==1」でかつ「b==2」の時だけです。
    また、「if文」や「for文」、「while文」などで、「[条件式1]と[条件式2]が[真]なら」 という判断をする場合、[条件式1]と[条件式2]の[偽]になる可能性が高い条件式を 先に記述する方が、処理速度が速くなることを意味しています。
import java.io.*;
import java.util.Random;

class Test{
    public static void main(String args[]){
        int    a;
        Random r = new Random();

        do{
            a = r.nextInt() % 30000;	// 乱数を作る
            System.out.println(a);
//      }while( a != 10 && a <= 20000 );
        }while( a <= 20000 && a != 10 );
    }
}
    上の例は、乱数を取得して、20000以下の数でかつ10でなければ繰り返すという プログラムです。一般的に考えて、「乱数20000以下」という条件と、「乱数が10でない」という 条件を考えた場合、「偽」になる可能性は前者の条件の方が高いです。したがって、 緑文字の「while文」よりも、青文字の「while文」の方が効率的であると言えます。


5章：演算子

11. 式と値

    C言語でもJavaでも「関数や式も値を持つ」という考え方を持っています。 すなわち「a = a + 1;」という文を考えたとき、変数「a」も、定数「1」も、 演算式「a + 1」も、式「a = a + 1」も値を持っています。 式「a = a + 1」の値は演算式「a + 1」と同じです。

    1章で「「a = b = 100;」は実は複雑な使い方です」と 書きました。前ページの表によると、「=が２つある場合は 右側の=が先に評価される」ことがわかります。したがって、「b = 100」 が最初に評価されます。上で解説したように、「b = 100」の値は [100]になるため、これが変数aに代入されます。その結果、変数aにも変数bにも 100が代入されます。

    では以下のような場合、aには何が代入されるでしょう？

    a = b == c;
    「=」と「==」を比較すると「==」の方が優先順位が高いので、式「b == c」が 評価されます。その結果が変数「a」に代入されます。したがって、bとcが 同じならaに「true」が、bとcが異なれば「false」が代入されます。つまり変数aはboolean型 である必要があります。C言語では、aはint型でも何でも良かったのですが、Javaでは型チェックが 厳しく行われるようになっており、バグの発生する可能性が少なくなっています。

    では、以下のような場合はどうなるでしょう？

    int a = 10, b = 10, c = 10;

    if( a == b == c )
        System.out.println("Hello");
    aもbもcも10が代入されているので、「Hello」と表示されそうですが、 [==]が２つあった場合左から評価されます。したがって「a==b」が評価され、同じなら true、異なるならfalseというboolean型の結果になります。次にその結果とcの値を評価しますが、 cはint型ですので、booleanとintを比較するということで、コンパイルエラーになります。 この辺もC言語とは異なるところです。


5章：演算子

12. 条件式と値

    条件式は「true」か「false」でプログラムの流れが変わることは説明しました。 C言語やC++では、条件式ではint型でもbool型(C++のみ)でも、条件になりました。 つまりCやC++では0ならばfalseで、それ以外ならばtrueと評価されていたからです。

    それはそれで便利なのですが、バグの原因ともなりえました。Javaではバグの要因はできるだけ 取り除く方針で開発された言語であるため、条件式にはboolean型しか記述することはできなくなりました。

    以上のことからC言語やC++ではOKだった以下の例はJavaでは用いることはできません。

    while(a != 0)
       ↓
    while(a)

    while(a == 0)
       ↓
    while(!a)


5章：演算子

13. 練習問題

問題
    次の２つのプログラムの実行結果を答えよ。また単項演算子を使用せず、複合代入演算子 を用いて書き換えよ。

import  java.io.*;

class Test{
    public static void main(String args[]){
        int i = 0;
        while( i++ < 10 )
            System.out.println(i);

    }
}

import  java.io.*;

class Test{
    public static void main(String args[]){
        int i = 0;
        while( ++i < 10 )
            System.out.println(i);

    }
}


5章：演算子

13. 練習問題

問題
    次の２つのプログラムの実行結果を答えよ。また単項演算子を使用せず、複合代入演算子 を用いて書き換えよ。

import  java.io.*;

class Test{
    public static void main(String args[]){
        int i = 0;
        while( i++ < 10 )
            System.out.println(i);

    }
}

import  java.io.*;

class Test{
    public static void main(String args[]){
        int i = 0;
        while( ++i < 10 )
            System.out.println(i);

    }
}

解答
1
2
3
4
5
6
7
8
9
10

import  java.io.*;

class Test{
    public static void main(String args[]){
        int i = 0;
        while( i < 10 ){
            i+=1;
            System.out.println(i);
        }
        i+=1;
    }
}

1
2
3
4
5
6
7
8
9

import  java.io.*;

class Test{
    public static void main(String args[]){
        int i = 0;
        i+=1;
        while( i < 10 ){
            System.out.println(i);
            i+=1;
        }
    }
}


6章：オブジェクト指向とは

1. オブジェクト指向とクラス

    C言語入門で作成したプログラムは 「手続き型」と呼ばれ、今となっては古い作成方法です。「手続き型」では、処理の流れによって プログラムを書いていく方法です。

    「オブジェクト指向」では、処理は二の次です。一番に考えるのはオブジェクトです。 では、オブジェクトとは何かが問題になります。オブジェクトというのは、「モノ」です。 直訳なので怒られそうですが、オブジェクトとは実際に存在するもののことをいうのです。 そしてクラスとオブジェクトとは、ほぼ等価です。

    そしてこのオブジェクトには決められた仕事や処理が決まっています。これがクラスで 定義するメンバーメソッドです。

    ここで色鉛筆を例に取りましょう。「色鉛筆」は実際に存在するモノです。したがって オブジェクトつまり「色鉛筆クラス」になるわけです(クラスという言葉に抵抗がある方は 「色鉛筆構造体だと思ってください)。色鉛筆には様々な色がありますので、メンバー変数 として「色」を決めましょう。そして、「芯の長さ」も含めましょう。

class Color{
    int R;
    int G;
    int B;
}

class ColorPen{
    Color  color;
    int   length;
}
    ところで、色鉛筆クラスは何をするんでしょう？　当然絵を描くことができます。 それではメンバーメソッドを定義しましょう。
class Color{
    int R;
    int G;
    int B;
}

class ColorPen{
    Color  color;
    int   length;

    void draw(){
      :
      :
    }
}
    クラスとは、こんな感じです。

    C言語では、構造体のメンバー変数とそれを処理する関数が別々に定義されていましたので、 例えば、「色鉛筆構造体」と「箸構造体」があり、「絵を描く」関数と、「食べ物を食べる」関数 があった場合、「色鉛筆で食べ物を食べる」とか「箸で絵をかく」という奇妙なプログラムも 書くことができます。

    「そんなバカな」と思われるかもしれませんが、大きなプログラムを複数の人が書く場合、 「そんなバカな」というようなミスが重大なバグを生む原因であることも少なくありません。

    オブジェクト指向では、このように「モノ」と「それを扱う関数(メソッド)」が一緒に クラスとして定義されていますので、「色鉛筆で食べ物を食べる」というミスは文法的に書 けないことになっています。

    整理しますと、「オブジェクト指向」では「モノ(主にデータやデバイス)を 第一優先に考える」という分析・設計・実装方法なのです。


6章：オブジェクト指向とは

2. 継承

    先ほどは「色鉛筆」の例を紹介しました。しかし色鉛筆は鉛筆の一種であると 考えられます。鉛筆でも絵を描くことができますが、色は黒一色です。色鉛筆では 様々な色で絵を描くことができます。つまり、、、
class Color{
    int R;
    int G;
    int B;
}

class Pencil{
    int  length;

    // 黒で絵をかく
    void draw(){
      :
      :
    }
}

class ColorPen extends Pencil{
    Color  color;
//  int  length;	スーパークラスで定義されているので、ここでは定義しない

    // colorで絵をかく
    void draw(){
      :
      :
    }
}
    このように定義すると、「色鉛筆クラスは鉛筆クラスの一種」という意味になります。 このように、既存のクラスの一種であるクラスを定義することを「継承」と言います。 色鉛筆クラスには「長さ」が定義されていませんが、鉛筆クラスで定義されていますので、 色鉛筆クラスにも「長さ」が定義されていることになります。

    この場合、色鉛筆クラスから見て鉛筆クラスを「スーパークラス」、 「基底クラス」あるいは、「親クラス」と言い、鉛筆クラスから見て 色鉛筆クラスを「サブクラス」あるいは「子クラス」と言います。

    実際にプログラムを作ると、クラスが増えていきます。複数のプログラマーが 作る場合には、どのようなクラス関係がなりたっているのかわかりにくくなりますので、 クラス関係を表すために「クラス図」と呼ばれる図を書くことをお薦めします。 この２つのクラスの関係は右図のようになります。

    もう１つ例を挙げましょう。

    上のクラス図は、基底クラスとして「哺乳類クラス」、その子クラスとして、 「犬クラス」「猫クラス」そして「象クラス」を定義している。さらに「猫クラス」の 子クラスとして「三毛猫クラス」「シャム猫クラス」を定義している。

    しかし、「犬クラス」の子クラスとして「盲導犬クラス」、「警察犬クラス」を 定義することはあまりよくありません。なぜならこの場合、動物の種類として継承している ため、そこに「警察犬」「盲導犬」といった機能で継承することになるからです。

    このように異なる分類で継承させる場合、特にJavaでは機能で継承させたい場合には、 継承ではなくインターフェースと言う機能を使います。インターフェースについては 後述します。


6章：オブジェクト指向とは

3. カプセル化

    クラスのメンバー変数、メンバーメソッドはだれが触っても良いものと、触るクラスを 限定したい場合もあります。誰が触っても良い物を「public」、自分と同じパッケージ内のクラスから もしくはサブクラスからしか触れない物を「protected」(C++のproctedとは機能が異なります)、そして 自分自身のクラスからしかさわれない物を「private」と言います。

    すべてのメンバー変数、メンバーメソッドをpublicにして、プログラマー達の決め事として 「この変数はパッケージ内のクラスからしか触っちゃダメね」と決めても良いのですが、中には決め事を 守らないプログラマーもいるため、public, protected, privateを決めておけば、文法的に コンパイラがチェックしてくれます。

    なぜ、そんなことをするのでしょう？　これは作ったプログラムの一部を部品化しやすいようにです。 オブジェクト指向では、作ったプログラムを部品化しておき、別のプログラムを作る際に再利用できるように します。例えばインターネットブラウザのプログラムを作った場合、ネットワーク関係の部分は、次に作る メーラーに再利用ができるわけです。このように部品化したクラスやパッケージは使い方さえ知っていれば そのソースコードを知らない人でも使うことができます。ところが、内部(ソースコード)を知らないくせに へんなことをやろうとするプログラマーが出てくるとバグの原因になります。そこで、protectedであれば、 勝手に変な使い方をされなくてすむわけです。

    そこで、基本的にメンバー変数はすべてprivateかprotectedにします。そして参照するためのメソッドと 代入するメソッドを用意します。このようなメソッドをアクセサメソッドと呼びます。なぜこんな面倒なことを するのか、主な理由は３つあります。

    １つめはメンバー変数の型を「int」から「double」に変更しようとした場合、参照する場所と、代入 する場所の２つに影響してきます。publicな変数ですと、プログラム全てに渡って、この変数 を参照する場所と代入する場所を探すのは面倒な作業です。それに対して、メソッドを探すのは、 比較的容易だからです。

    ２つめは、このメンバー変数は他のクラスから参照しても良いけど、値を変更するのは ダメ、というような変数があったとします。この場合、参照するメソッドはpublicに、代入する メソッドはprotectedかprivateにすればよいのです。

    最後に、マルチスレッドプログラムにした場合です。マルチスレッドプログラムでは、 複数のスレッドが同じ変数にアクセスする場合があります。例えば、あるスレッドがリストを ソート中に、別のスレッドがそのリストを参照した場合、矛盾が生じてしまいます。 このような場合は、「セマフォ」という、リストを参照する権利を与え、権利があるスレッド だけがリストを参照したり、いじったりできるようにします。あらかじめ参照するためのメソッドを 用意しておけば、セマフォの処理を組み込む場合も、そのメソッドをちょっと直すだけなので、 簡単です。

    このように、変数やメソッドにアクセスするできるクラスを制限することで、あるクラスを 部品のようにすることができます。部品のようになったクラスは別のプログラムを作るときにも 利用できます。例えば、Javaでは標準でGUIや通信ｍその他さまざまなクラスがたくさん用意されており、 通信をしたいと思えばそのためのクラスを、ファイル操作をしたければそのためのクラスを、 プログラマーが使用するだけですので、プログラムが簡単に書けます。また、携帯電話用の ソフトを書きたければ、携帯電話会社が公開しているクラス群が利用できるのです。

    また、通信をするためのクラスを利用しようと思った場合、通常その中身がどんな プログラムになっているのか、使う人(プログラマー)は知る必要がありません。つまり 通信をするためのクラスの中身はブラックボックスということになります。

    このように、中身を知らなくても部品のように使えるように設計することを 「カプセル化」と言います。


6章：オブジェクト指向とは

4. ポリモーフィズム

    ポリモーフィズムとはギリシア語で「多数の形態」という意味です。プログラムで 「ポリモーフィズム」と言うと、同じ名前のメソッドを定義することを言います。

    先ほどの例に例えると、「鉛筆クラス」の「絵を描くメソッド」と「色鉛筆クラス」の 「絵を描くメソッド」は、機能は若干異なりますが、「絵を描くメソッド」というメソッドの名前は 同じです。

    例えば、int型の絶対値を求めるメソッドを「abs」と名付けると、小数も扱えるような メソッドを追加しようとした場合、「abs_float」と名前を変えなくてはいけませんでした。 Javaでは、ポリモーフィズムによって、引数が異なれば同じ名前のメソッドを複数定義することも できます。このように同じ名前のメソッド(やコンストラクタ)を作ることを、メソッドをオーバーロードする と言います。


7章：クラス

1. thisキーワード

    ４章で作成したMyPointクラスを例にとり、復習をする。
MyPoint.java

class MyPoint{
    double  m_x;
    double  m_y;

    // コンストラクタ
    public MyPoint(){
        System.out.println("コンストラクタ１が呼ばれました");
        m_x = 10;
        m_y = 20;
    }

    // コンストラクタ2
    public MyPoint(double x, double y ){
        System.out.println("コンストラクタ２が呼ばれました");
        m_x = x;
        m_y = y;
    }

    // 文字列に変換する
    public String toString(){
        return "X:"+m_x+"  Y:"+m_y;
    }

    // 距離を計算する
    public double calcDistance(MyPoint pnt){
        // Mathクラスは数学関係を担うクラス
        // Math.pow累乗を計算するメソッド
        // Math.sqrtはルートを計算するメソッド
        return Math.sqrt(Math.pow((m_x - pnt.m_x), 2) + Math.pow((m_y - pnt.m_y), 2));
    }
}

    まず、4-3で解説したようにコンストラクタはインスタンスが作られるときに 最初に呼ばれるめそっである。MyPointクラスでは引数をとるコンストラクタと引数をとらないコンストラクタの ２種類が用意されている。メソッドでもそうだが、引数の異なる同じ名前のメソッドを複数用意することを 4-6で解説したように、メソッドを(コンストラクタを)オーバーロードすると言う。

    引数なしのコンストラクタは変数「m_x」と「m_y」にそれぞれ10と20を代入しているが、これは引数ありの コンストラクタに10,20を渡しても同じ結果が得られる。コンストラクタから同じクラス内の別のコンストラクタ を呼び出すこともできる。もっと複雑なクラスになると、コンストラクタを複数用意する場合、ある一部を除いて 同じことをする。このため、あるコンストラクタから別のコンストラクタを呼び出すと便利な場合が多い。その方法は 以下のように「this」キーワードを用いる。ただし、コンストラクタの一番最初で使用しなければならない。
MyPoint.java

class MyPoint{
    double  m_x;
    double  m_y;

    // コンストラクタ
    public MyPoint(){
        this(10,20);
        System.out.println("コンストラクタ１が呼ばれました");
//      m_x = 10;
//      m_y = 20;
    }

    // コンストラクタ2
    public MyPoint(double x, double y ){
        System.out.println("コンストラクタ２が呼ばれました");
        m_x = x;
        m_y = y;
    }
     :
     :
    省略
     :
     :
}

    calcDistanceメソッドでは、引数にMyPointクラスをもらっている。この場合、同じ名前の変数(m_x, m_y)が 頻繁に出てくるため、プログラムを見直した場合、自分自身のインスタンスの変数か、引数でもらってきた インスタンスの変数か紛らわしくなることがある。この場合、thisというキーワードをつけて明示的に 区別することができる。したがって、下記のように書き直すこともできる。(変数だけでなく、メソッドにも thisをつけることもできる。)
MyPoint.java

class MyPoint{
     :
     :
    省略
     :
     :
    // 距離を計算する
    public double calcDistance(MyPoint pnt){
        // Mathクラスは数学関係を担うクラス
        // Math.pow累乗を計算するメソッド
        // Math.sqrtはルートを計算するメソッド
        return Math.sqrt(Math.pow((this.m_x - pnt.m_x), 2) + Math.pow((this.m_y - pnt.m_y), 2));
    }
}


7章：演算子

2. public, protected, private

    6-3で、public, protected, privateについて簡単に述べた。 4-4の例題で作成したIntStackクラスでは、メンバー変数を勝手にいじられると誤動作する可能性がある。 そこで、この変数をprivateにしたほうがバグの可能性が少なくなる。

    また、前ページで解説したように、２つのコンストラクタでは、 同じような操作をしているため、この辺も整理したIntStackクラスは以下のようになる。

IntStack.java
public class IntStack{
    ////////////////////////////////////////////////////
    // 管理情報

    private int m_sp;	// スタックポインタ：次にプッシュする位置
    private int m_size;	// スタックサイズ
    private int m_stack[];	// スタック

    // コンストラクタ：引数あり
    // IntStack オブジェクトが定義された時に自動的に呼び出される
    public IntStack( int sz ){
        // スタックサイズを保存する
        m_size = sz ;

        // スタックの実体(size個のint配列)を自由記憶上に割り当てる
        m_stack = new int[m_size] ;

        // スタックポインタを初期化する
        m_sp = 0 ;
    }

    // コンストラクタ：引数なし
    // IntStack オブジェクトが定義された時に自動的に呼び出される
    public IntStack( ){
        // もう１つのコンストラクタを呼び出す
        this(100);
    }

    // プッシュ
    public void push( int value ){
        if( m_sp >= m_size ){
            System.err.println("stack overflow");
            System.exit(1) ;        // 異常終了する
        }

        m_stack[m_sp] = value ;
        m_sp = m_sp + 1 ;
    }

    // ポップ
    public int pop(){
        // アンダーフローのチェック
        if( m_sp <= 0 ){
            System.err.println("stack underflow");
            System.exit(1) ;        // 異常終了する
        }

        m_sp = m_sp - 1;
        return m_stack[m_sp];
    }

    // 現在のスタック長を獲得する
    public int getLength(){
        return m_sp;
    }

    // 指定位置(スタックトップからのオフセット)のスタック要素を覗き見る
    public int peek(){
        // オフセットのチェック
        if( 0 >= m_sp ){
            System.err.println("stack underflow");
            System.exit(1) ;        // 異常終了する
        }

        // 指定位置の要素をコピーする
        return m_stack[m_sp-1];
    }
}
    なお、以前のIntStackクラスでは、メンバー変数に対して、private, protected, public いずれも指定していなかった。この場合、同じパッケージのクラスからのみ扱える。（protectedはそれに加えて サブクラスでも扱える。）

    今のところ、パッケージについては後述するが、パッケージを指定しない場合は、同じファイルに複数の クラスを作成したときに、同じファイル内で定義されているクラスのみから扱えるようになる。

    public, protected, provateに関して整理すると以下の表のようになる。

変数・メソッドの修飾子
修飾子 | 意　　味
未指定 同じパッケージ
パッケージ未指定の場合は、同じファイル内に定義したクラス
public | すべてのクラス
protected 同じパッケージ
サブクラス
private | 同一クラスのみ


7章：演算子

3. 静的変数・静的メソッド・静的ブロック

静的変数

    これまでインスタンスを作ると、それに伴い変数も複数用意された。すなわち、インスタンス毎に変数が 用意されたわけである。静的変数というのは、そのクラスに１つだけの変数で、複数のインスタンスを作っても １つしか作られない変数です。インスタンスを作らなくても使用できる変数です。主に定数として使います。

    静的変数を作るには、変数宣言の前に「static」とつけるだけです。

    静的変数もメンバー変数の一種なので、前述の通り、0で初期化されます。

Test.java
class A{
    int        a;
    static int s;
}

class Test{
    public static void main(String args[]){
        // インスタンスを作らなくても、静的変数は使える
        A.s = -10;
        System.out.println("A.s=" + A.s);

        A a1 = new A();
        A a2 = new A();

        System.out.println("a1.a=" + a1.a + "\ta1.s=" + a1.s);

        a1.a = 10;
        a2.a = 20;
        a1.s = 100;
        a2.s = 200;

        System.out.println("a1.a=" + a1.a + "\ta1.s=" + a1.s);
        System.out.println("a2.a=" + a2.a + "\ta2.s=" + a2.s);
    }
}

C:\java>javac Test.java

C:\java>java Test
A.s=-10
a1.a=0  a1.s=-10
a1.a=10 a1.s=200
a2.a=20 a2.s=200

C:\java>
    上記の例では、a2.sに200を代入しているにもかかわらず、a1.sも200になっています。静的変数は、 複数のインスタンスを作成しても、全てのインスタンスが同じ変数を共通で使用するためです。

静的メソッド

    静的メソッドは、インスタンスに作用するメソッドではなく、クラスに作用するメソッドです。 つまり、静的変数のみ使用することができ、通常のインスタンス変数を扱うことはできません。 また、同一クラスの他の静的メソッドを呼び出すことはできますが、通常のインスタンスメソッドを 呼び出すことはできません。(通常のインスタンスメソッドから静的メソッドを呼び出すことはできます。)

    静的メソッドは、クラスに作用するメソッドですので、インスタンスが作られていなくても使用することも できます。

    静的メソッドを作るには、メソッド宣言の前に「static」をつけるだけです。

Test.java
class A{
    int        a;
    static int s;

    // 静的メソッド
    static void hoge(){
        s = -10;

        // 静的変数から、インスタンス変数にアクセスできないので、コンパイルエラー
//      a = -10;
    }
}

class Test{
    public static void main(String args[]){
        // インスタンスを作らなくても、静的変数は使える
        System.out.println("A.s=" + A.s);
        A.hoge();
        System.out.println("A.s=" + A.s);

        A a = new A();

        a.a = 10;
        a.s = 100;

        System.out.println("a.a=" + a.a + "\ta.s=" + a.s);
    }
}

C:\java>javac Test.java

C:\java>java Test
A.s=0
A.s=-10
a.a=10  a.s=100

C:\java>

静的ブロック

    インスタンス変数を初期化するためにコンストラクタがあるように、静的変数を初期化するために 静的ブロックというものがあります。これは、そのクラスが使用される直前に呼び出されます。 クラスを作っている段階では、どのようにそのクラスが使われるかわからないので、どのような順番で 実行されてもよいように作らなければなりません。つまり、自分のクラスの静的変数の初期化のみを 行うのが一般的です。

    静的ブロックは「static」の後にブロックを作ります。

Test.java
class A{
    int        a;
    static int s;

    // 静的ブロック
    static{
        System.out.println("Aクラスの静的ブロック");
    }
}

class Test{
    // 静的ブロック
    static{
        System.out.println("Testクラス静的ブロック");
    }

    public static void main(String args[]){
        System.out.println("mainメソッド実行開始");

        A.s = 10;
    }
}
Test.java
class A{
    int        a;
    static int s;

    // 静的ブロック
    static{
        System.out.println("Aクラスの静的ブロック");
    }
}

class Test{
    // 静的ブロック
    static{
        System.out.println("Testクラス静的ブロック");

        A.s = 10;
    }

    public static void main(String args[]){
        System.out.println("mainメソッド実行開始");
    }
}

C:\java>javac Test.java

C:\java>java Test
Testクラス静的ブロック
mainメソッド実行開始
Aクラスの静的ブロック

C:\java>

C:\java>javac Test.java

C:\java>java Test
Testクラス静的ブロック
Aクラスの静的ブロック
mainメソッド実行開始

C:\java>
    Aクラスの使用する場所が異なると、実行される順番が変わることがわかります。


7章：演算子

4. ローカル変数

    メンバー変数(インスタンス変数や静的変数)はクラス定義内で定義していました。ローカル変数とはこれまでにも 何度か登場していますが、メソッド内など、ブロック内(中括弧の中)で定義されている変数です。

class Test{
    public static void main(String args[]){
        int i;	// これがローカル変数
    }
}

    メンバー変数では初期値が設定されていましたが、ローカル変数では初期値がありません。CやC++では、 不確定な値が代入されていますが、Javaでは不確定な値を使用しようとするとコンパイル時にエラーが出ます。

class Test{
    public static void main(String args[]){
        int i;

        // 不確定な変数を使用することはできないのでコンパイルエラー
        System.out.println(i);
    }
}

C:\java>javac Test.java
Test.java:6: Variable i may not have been initialized.
        System.out.println(i);
                           ^
1 error

C:\java>

    ローカル変数は、定義されたブロック内でしか使用することができません。変数を使用できる範囲のことを スコープと言います。Cではブロックの最初でしか変数を定義できませんでしたが、JavaやC++ではブロックのどこ でも宣言できます。しかし宣言したブロックを出ると、スコープの範囲外になり、その変数は使用することはできません。

class Test{
    public static void main(String args[]){
        System.out.println("mainメソッド");

        // ブロックの途中でも宣言できる
        int i = 0;

        for( ; i < 5 ; i++ ){
            int j = 0;
            // ブロック内であればその中のブロックの中でもしようできる
            System.out.println(i);
        }
        // ローカル変数[j]はスコープを出ているのでコンパイルエラー
        System.out.println(j);
    }
}

C:\java>javac Test.java
Test.java:14: Undefined variable: j
        System.out.println(j);
                           ^
1 error

C:\java>

    また、特例として、for文の初期設定で変数を宣言することができます。この場合、そのfor文(を含むブロック)がスコープになります。

class Test{
    public static void main(String args[]){
        System.out.println("mainメソッド");

        // for文の初期設定でもローカル変数を宣言できる
        for( int i = 0 ; i < 5 ; i++ ){
            // ローカル変数[i]はfor文(ブロック)でのみ有効だから、ここではOK!
            System.out.println(i);
        }
        // ローカル変数[i]はスコープを出ているのでコンパイルエラー
        System.out.println(i);
    }
}

C:\java>javac Test.java
Test.java:11: Undefined variable: i
        System.out.println(i);
                           ^
1 error

C:\java>

    ローカル変数は、メンバー変数と同じ名前で使用することができる。この場合、ローカル変数のスコープないでは ローカル変数が使用され、スコープをはずれるとメンバー変数が使用される。つまり、そのスコープ内では、 メンバー変数が隠されることになる。この場合、インスタンス変数の場合は「this」キーワードを、 静的変数の場合は「クラス名.」を使用することで、隠されているメンバー変数にアクセスできる。

class Test{
    // 静的変数として[m]を定義
    static int m;

    public static void main(String args[]){
        System.out.println("m=" + m);

        {
            // 静的変数とし同じ[m]を定義
            int m = 10;
            System.out.println("m=" + m);

            // 静的変数の場合は[クラス名.]を使用する
            System.out.println("Test.m=" + Test.m);
        }

        System.out.println("m=" + m);
    }
}

class Test{
    // インスタンス変数として[m]を定義
    int m;

    private void hoge(){
        System.out.println("m=" + m);
        {
            m = 1;	// this.m = 1 と同じ

            // インスタンス変数とし同じ[m]を定義
            int m = 10;
            System.out.println("m=" + m);

            // インスタンス変数の場合は[this.]を使用する
            System.out.println("this.m=" + this.m);
        }

        System .out.println("m=" + m);
    }

    public static void  main(String args[]){
        new Test().hoge();
    }
}

C:\java>javac Test.java

C:\java>java Test
m=0
m=10
Test.m=0
m=0

C:\java>

C:\java>javac Test.java

C:\java>java Test
m=0
m=10
this.m=1
m=1

C:\java>

class Test{
    public static void main(String args[]){
        int a = 10;
        {
            // これはエラー
            int a = 10;
        }
    }
}

C:\java>javac Test.java
Test.java:6: Variable 'a' is already defined in this method.
            int a = 10;
                ^
1 error

C:\java>


7章：演算子

5. メソッドの引数

    メソッドへ引数を渡す場合、渡した先のメソッドで値を変えられたらどうなるのでしょう？
class Test{
    public static void main(String args[]){
        int i = 10;
        increment(i);

        System.out.println(i);
    }

    private static void increment(int i){
        i++;
    }
}

C:\java>javac Test.java

C:\java>java Test
10

C:\>
    上の例のように、incrementメソッドに10が代入されている変数[i]を渡して、メソッドの中で値を 変えても、呼び出し元に戻ってきてもその値は変わっていません。これは他の言語を知っている人には ごく普通のことだと思います。

    では、渡す引数が配列だった場合はどうでしょう？

class Test{
    public static void main(String args[]){
        int array[] = { 1,2,3,4,5,6,7,8,9,10 };
        increment(array);

        for( int i = 0 ; i < array.length ; i++ )
            System.out.println("array[" + i + "]=" + array[i]);
    }

    private static void increment(int[] array){
        for( int i = 0 ; i < array.length ; i++ )
            array[i]++;
    }
}

C:\java>javac Test.java

C:\java>java Test
array[0]=2
array[1]=3
array[2]=4
array[3]=5
array[4]=6
array[5]=7
array[6]=8
array[7]=9
array[8]=10
array[9]=11

C:\>
    配列の場合は、配列の中身が変わってしまっています。

    ではもうひとつ、引数がクラスのインスタンスだった場合はどうでしょう？

class Int{
    int i;

    // コンストラクタ
    public Int(int n){
        i = n;
    }
    // コンストラクタ
    public Int(){
        this(0);
    }

    public void increment(){
        i++;
    }
    public void decrement(){
        i--;
    }

    public int intValue(){
        return i;
    }

    public String toString(){
        return "" + i;
    }
}

class Test{
    public static void main(String args[]){
        Int i = new Int();

        System.out.println(i.toString());
        increment(i);
        System.out.println(i.toString());
    }

    private static void increment(Int i){
        i.increment();
    }
}

C:\java>javac Test.java

C:\java>java Test
0
1

C:\>
    このように、インスタンスの場合も値が変わってしまいます。

    C++を知っている人ならば「あっ、参照渡しなのね。」ということで納得できると思います。参照渡しというのは、 インスタンスそのものを渡すので、メソッドの先で値を変えると、それが呼び出し元にも反映されるのです。 実はJavaでは、int型やdouble型などのプリミティブ型では、CやC++と同じでコピー渡しをします。コピーですので、 呼び出し先のメソッドで変更しても、所詮コピーなのでその結果は呼び出し元には影響しません。しかし、 配列やクラスのインスタンスの場合は参照渡しという方法をとっているので、呼び出し先で変更されると、 それが呼び出し元にも影響されます。

    実は「参照渡し」と言っているにもかかわらず、どちらかというとCやC++の言葉を借りるのならば、 「アドレス渡し」に近いです。つまり、配列にしてもインスタンスにしても、その中身はメモリーのどこかに 格納されているわけですが、そのアドレスが引数として呼び出し先に渡されます。呼び出し先では、 渡されたアドレスが指しているインスタンスや配列を変えてしまえば、呼び出し元に戻ってきても、その変更が 影響されます。

    では、次に呼び出し先でnewしなおした場合はどうでしょう？

class Test{
    public static void main(String args[]){
        int array[] = { 1,2,3,4,5,6,7,8,9,10 };
        increment(array);

        for( int i = 0 ; i < array.length ; i++ )
            System.out.println("array[" + i + "]=" + array[i]);
    }

    private static void increment(int[] array){
        array = new int[10];
        for( int i = 0 ; i < array.length ; i++ )
            array[i] = i+2;
    }
}

C:\java>javac Test.java

C:\java>java Test
array[0]=1
array[1]=2
array[2]=3
array[3]=4
array[4]=5
array[5]=6
array[6]=7
array[7]=8
array[8]=9
array[9]=10

C:\>

class Int{
    int i;

    // コンストラクタ
    public Int(int n){
        i = n;
    }
    // コンストラクタ
    public Int(){
        this(0);
    }

    public void increment(){
        i++;
    }
    public void decrement(){
        i--;
    }

    public int intValue(){
        return i;
    }

    public String toString(){
        return "" + i;
    }
}

class Test{
    public static void main(String args[]){
        Int i = new Int();

        System.out.println(i.toString());
        increment(i);
        System.out.println(i.toString());
    }

    private static void increment(Int i){
        i = new Int(i.intValue() + 1);
    }
}

C:\java>javac Test.java

C:\java>java Test
0
0

C:\>
    このように、呼び出し先でnewしなおした場合は、結果は反映されていません。変数は、本体(配列の実態など)が 格納されているアドレスを持っているに過ぎず、引数はそのアドレスのコピーが渡されるからです。つまり、 newしなおして、アドレスが変更されても、変更されたアドレスはコピーにしか過ぎず、呼び出し元はnewする前の 古いアドレスを持っているからです。


7章：演算子

6. null

    クラスや配列は「new」して使用することは前述しました。下記のように変数だけ宣言して、newしないで 使用しようとするとコンパイルエラーが出ることも説明しました。
class Test{
    public static void main(String args[]){
        String s;
        System.out.println(s);
    }
}

C:\java>javac Test.java
Test.java:4: Variable s may not have been initialized.
        System.out.println(s);
                           ^

C:\java>
    これはインスタンスを入れるための入れ物(変数)であるsには、まだインスタンスが入っていないのに 使用しようとしたためです。インスタンスを使うためにはインスタンスを作らなければなりません。これは 前述したとおりです。

    逆に、使い終わったインスタンスはどうすればよいのでしょう？C++でが「delete」という削除するための命令があり、 使い終わったらdeleteしなくてはなりませんでした。ところが、C++ではdeleteしてしまった後も、インスタンスを 使おうとすることができてしまいます。この場合バグになってしまいます。javaではこのようなバグを起こさないように するために、deleteに相当するものはありません。

    使い終わった場合は、使い終わったことを示すために、入れ物(変数)にインスタンスがないことを示す値を 代入します。これが「null」です。

class Test{
    public static void main(String args[]){
        String s = new String("abcdefg");
        System.out.println(s);

        // 使い終わったことを示す
        s = null;
    }
}

C:\java>javac Test.java

C:\java>java Test
abcdefg

C:\java>

    このnullは配列にも同じように使用します。

class Test{
    public static void main(String args[]){
        int a[] = new int[10];

        // 使い終わったことを示す
        a = null;
    }
}

C:\java>javac Test.java

C:\java>java Test

C:\java>

    使い終わった場合にnullを代入すると言うのは、強制ではありません。例えば上の例でnullを代入しなくても、 エラーは出ませんし、バグも起きません。またプログラムが終了すれば、自動的に使用していたメモリーも開放するため、 メモリーリーク(メモリーにゴミが残る現象)もありません。

    しかし、javaでは後述しますが、ガーベージコレクターというメモリーを掃除する機能があります。この ガーベージコレクターは、暇を見つけてはいらないインスタンスを消してくれます。使い終わったインスタンスを nullに置き換えないと、このガーベージコレクターはそのインスタンスが使用中なのかわからないため、掃除すること ができません。したがってnullを代入することが推奨されています。

    また、nullは使い終わったことを示すだけではなく、インスタンスがないことを示すので、 使い終わったことを示すだけではなく「インスタンスが無い」ということを示すような使い方をすることも できます。

    例えば、引数にint型のnをもらい、アルファベットのn番目の文字をString型で返すメソッドを作ります。 該当する文字がない場合は、nullを返します。

class Test{
    private static String alphabet(int n){
        String s;
        if( n < 1 || n > 26 ){
            // 該当する文字がない場合はnullを返す
            s = null;
        }
        else{
            char ch[] = {'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'};

            // char配列chのn-1番めから1文字分の文字列を作る
            s = new String(ch, n-1, 1);
        }
        return s;
    }

    private static void print(String s){
        if( s == null )
            System.err.println("nullです");
        else
            System.out.println(s);
    }

    public static void main(String args[]){
        int n = 0;
        String s;

        s = alphabet(0);
        print(s);

        s = alphabet(26);
        print(s);
    }
}

C:\java>javac Test.java

C:\java>java Test
nullです
Z

C:\java>


7章：演算子

7. 練習問題

問題
    標準入力から１行ずつ読み込み、逆順にして表示するプログラムを作成する。Ctrl + Zが押されたら 終了する。まず、IntStackクラスを参考にして、StringStackクラスを 作成する。

    また、標準入力から１行ずつ読み込むメソッドは以下のものを使用する。このメソッドを使用した場合、 Ctrl + Zが押された場合に返される文字列の長さが0になる。文字列の長さを取得するには、Stringクラスの publicメソッドである「length()」メソッドを使用する。

// １行読み込んで、String型として返す
public static String readLine(){
    byte b[] = new byte[1024];
    try{
        System.in.read(b);
    }catch(Exception e){
        return null;
    }

    int i;
    for( i = 0 ; b[i] != 0 && i < b.length ; i++ );
    return new String(b, 0, i);
}


7章：演算子

7. 練習問題

問題
    標準入力から１行ずつ読み込み、逆順にして表示するプログラムを作成する。Ctrl + Zが押されたら 終了する。まず、IntStackクラスを参考にして、StringStackクラスを 作成する。

    また、標準入力から１行ずつ読み込むメソッドは以下のものを使用する。このメソッドを使用した場合、 Ctrl + Zが押された場合に返される文字列の長さが0になる。文字列の長さを取得するには、Stringクラスの publicメソッドである「length()」メソッドを使用する。

// １行読み込んで、String型として返す
public static String readLine(){
    byte b[] = new byte[1024];
    try{
        System.in.read(b);
    }catch(Exception e){
        return null;
    }

    int i;
    for( i = 0 ; b[i] != 0 && i < b.length ; i++ );
    return new String(b, 0, i);
}

解答
RevLine.java

class RevLine{
    public static void main(String args[]){
        StringStack stack = new StringStack(1000);

        String str;
        while(true){
            str = readLine();
            if( str.length() == 0 )
                break;

            stack.push(str);
        }

        System.out.println("--end--");

        while(stack.getLength() > 0 )
            System.out.print(stack.pop());
    }

    // １行読み込んで、String型として返す
    public static String readLine(){
        byte b[] = new byte[1024];
        try{
            System.in.read(b);
        }catch(Exception e){
            return null;
        }

        int i;
        for( i = 0 ; b[i] != 0 && i < b.length ; i++ );
        return new String(b, 0, i);
    }
}

StringStack.java

public class StringStack{
    ////////////////////////////////////////////////////
    // 管理情報

    private int m_sp;		// スタックポインタ：次にプッシュする位置
    private int m_size;		// スタックサイズ
    private String m_stack[];	// スタック

    // コンストラクタ：引数あり
    // StringStack オブジェクトが定義された時に自動的に呼び出される
    public StringStack( int sz ){
        // スタックサイズを保存する
        m_size = sz ;

        // スタックの実体(size個のint配列)を自由記憶上に割り当てる
        m_stack = new String[m_size] ;

        // スタックポインタを初期化する
        m_sp = 0 ;
    }

    // コンストラクタ：引数なし
    // StringStack オブジェクトが定義された時に自動的に呼び出される
    public StringStack( ){
        // もう１つのコンストラクタを呼び出す
        this(100);
    }

    // プッシュ
    public void push( String value ){
        if( m_sp >= m_size ){
            System.err.println("stack overflow");
            System.exit(1) ;        // 異常終了する
        }

        m_stack[m_sp] = value ;
        m_sp++;
    }

    // ポップ
    public String pop(){
        // アンダーフローのチェック
        if( m_sp <= 0 ){
            System.err.println("stack underflow");
            System.exit(1) ;        // 異常終了する
        }

        m_sp--;
        return m_stack[m_sp];
    }

    // 現在のスタック長を獲得する
    public int getLength(){
        return m_sp;
    }

    // 指定位置(スタックトップからのオフセット)のスタック要素を覗き見る
    public String peek(){
        // オフセットのチェック
        if( 0 >= m_sp ){
            System.err.println("stack underflow");
            System.exit(1) ;        // 異常終了する
        }

        // 指定位置の要素をコピーする
        return m_stack[m_sp-1];
    }
}


7章：演算子

8. インナークラス(内部クラス)

    インナークラス(C++でいう内部クラス)は、あるクラスの中で定義するクラスです。 あるクラスの中でのみ使用するデータ群をまとめてしまうのに便利です。例えばint型の 数字10個からなるデータがあるとします。しかし10個ものデータを別個に管理するのは 面倒なので、int型10個の数値を１つのクラスとしてまとめてしまうことにしました。 このデータをDataクラスとします。しかしこのDataクラスは他のクラスからは 参照されないとすると、publicのスコープでDataクラスを宣言する必要はないので、 インナークラスとして宣言します。

    もし、複数の人で１つのプログラムを作っていたとします。publicなスコープにDataクラスを 宣言すると、他の人はDataクラスと言うクラスを作ることができなくなってしまいます。もし、 作ってしまうとバグにつながってしまいます。したがって、できるだけpublicなスコープで、 クラスなどは作らないようにするべきなのです。

class Test{
    // インナークラス
    class Data{
        int a1,a2,a3,a4,a5,a6,a7,a8,a9,a10;
    }

    public static void main(String args[]){
        Data data = new Data();
    }
}

    もう１つの利用法として(Javaではこちらの用途の方が多い)、イベントを受けとる クラスとして定義します。つまりデータだけではなく、あるクラスの機能のために 作る場合もあります。なお、この用途の使い方は後述します。


7章：演算子

9. 無名インナークラス(無名内部クラス)

    無名インナークラスとは、インナークラスをもっと簡略したもので、名前を持たないインナークラスです。 インスタンスを作る時に(newする時に)、クラスの宣言を行ってしまうもので、ごくごく簡単なクラスを 作る時に使用します。

    主にイベントのアダプタクラスとして使用します。詳しくは後述します。


7章：演算子

10. クラスの包含

    これまでなんとなくクラスのインスタンスを作ってきましたが、クラスとクラスの関係について少し詳しく解説します。

    クラスの包含とは、どのようにあるクラスが別のクラスのインスタンスを持つかと言う関係を表します。

class A_Class{
    B_Class B;

    // コンストラクタ
    public A_Class(){
        B = new B_Class()
    }
}

class A_Class{
    B_Class pB;
}
A_Classのインスタンスが存在する間はずっとB_Classのインスタンスも存在する。 逆にB_Classのインスタンスがなくなってしまえば、A_Classは機能しなくなるような場合に 用いられる。

このような場合はコンストラクタでインスタンスが作られる場合が多い。

A_Classのインスタンスが存在する間も、B_Classのインスタンスが存在する場合と 存在しない場合があるような場合に用いられる。

この場合、インスタンスは適当なところで作られる。

例）自動車クラスはエンジンクラスを包含するなど。
(この場合、エンジンがなくなれば自動車ではなくなる) 		例）自動車クラスは運転手クラスを包含するなど。
(この場合、運転手がいてもいなくても、自動車は自動車であることにかわりはない)

    上の用途の例はあくまでも例であり、用途やプログラマー(設計者)が一番適切な 包含の仕方を決める。


7章：演算子

11. 練習問題

問題
    標準入力から１行ずつ読み込み、最後の５行を表示するプログラムを作成する。Ctrl + Zが押されたら 終了する。まず、IntStackクラスを参考にして、StringVectorクラスを 作成する。そして、このStringVectorクラスを包含してStringRingBufferクラスを作成する。

    また、標準入力から１行ずつ読み込むメソッドは以下のものを使用する。このメソッドを使用した場合、 Ctrl + Zが押された場合に返される文字列の長さが0になる。文字列の長さを取得するには、Stringクラスの publicメソッドである「length()」メソッドを使用する。

// １行読み込んで、String型として返す
public static String readLine(){
    byte b[] = new byte[1024];
    try{
        System.in.read(b);
    }catch(Exception e){
        return null;
    }

    int i;
    for( i = 0 ; b[i] != 0 && i < b.length ; i++ );
    return new String(b, 0, i);
}


7章：演算子

11. 練習問題

問題
    標準入力から１行ずつ読み込み、最後の５行を表示するプログラムを作成する。Ctrl + Zが押されたら 終了する。まず、IntStackクラスを参考にして、StringVectorクラスを 作成する。そして、このStringVectorクラスを包含してStringRingBufferクラスを作成する。

    また、標準入力から１行ずつ読み込むメソッドは以下のものを使用する。このメソッドを使用した場合、 Ctrl + Zが押された場合に返される文字列の長さが0になる。文字列の長さを取得するには、Stringクラスの publicメソッドである「length()」メソッドを使用する。

// １行読み込んで、String型として返す
public static String readLine(){
    byte b[] = new byte[1024];
    try{
        System.in.read(b);
    }catch(Exception e){
        return null;
    }

    int i;
    for( i = 0 ; b[i] != 0 && i < b.length ; i++ );
    return new String(b, 0, i);
}

解答
TailLine.java

class TailLine{
    public static void main(String args[]){
        StringRingBuffer buffer = new StringRingBuffer(5);

        String str;
        while(true){
            str = readLine();
            if( str.length() == 0 )
                break;

            buffer.add(str);
        }

        System.out.println("--end--");

        for( int i = 0 ; i < buffer.getLength() ; i ++ )
            System.out.print(buffer.get(i));
    }

    // １行読み込んで、String型として返す
    public static String readLine(){
        byte b[] = new byte[1024];
        try{
            System.in.read(b);
        }catch(Exception e){
            return null;
        }

        int i;
        for( i = 0 ; b[i] != 0 && i < b.length ; i++ );
        return new String(b, 0, i);
    }
}

StringVector.java

public class StringVector{
    ////////////////////////////////////////////////////
    // 管理情報

    protected int m_size;            // 配列サイズ
    protected String m_vector[];     // 配列

    // コンストラクタ：引数あり
    // StringVector オブジェクトが定義された時に自動的に呼び出される
    public StringVector( int sz ){
        // 配列サイズを保存する
        m_size = sz ;

        // 配列の実体(size個のint配列)を自由記憶上に割り当てる
        m_vector = new String[m_size] ;
    }

    // コンストラクタ：引数なし
    // StringVector オブジェクトが定義された時に自動的に呼び出される
    public StringVector( ){
        // もう１つのコンストラクタを呼び出す
        this(100);
    }

    // 代入
    public void at( int n, String value ){
        if( n >= m_size || n < 0 ){
            System.err.println("invalid index");
            System.exit(1) ;        // 異常終了する
        }

        m_vector[n] = value ;
    }

    // 取得
    public String at(int n){
        if( n >= m_size || n < 0 ){
            System.err.println("invalid index");
            System.exit(1) ;        // 異常終了する
        }

        return m_vector[n];
    }

    // 現在の配列サイズを獲得する
    public int getLength(){
        return m_size;
    }
}

StringRingBuffer.java

public class StringRingBuffer{
    StringVector    m_vect;          // 配列本体
    int             m_nSize;         // バッファーサイズ
    int             m_nIndexAdd;     // 次に格納する場所を表すインデックス
    int             m_nIndexGet;     // 一番古いデータが格納されているインデックス
    int             m_nLength;       // 現在格納されている要素数

    public StringRingBuffer(int size){
        m_vect      = new StringVector(size);
        m_nSize     = size;
        m_nIndexAdd = 0;
        m_nIndexGet = 0;
        m_nLength   = 0;
    }

    // データを格納する
    void add(String value){
        // データを格納
        m_vect.at(m_nIndexAdd++, value);

        // 次に格納するインデックスを更新する
        m_nIndexAdd %= m_nSize;

        // 格納されている要素数を増やす
        if( m_nLength < m_nSize )
            m_nLength++;

        // もっとも古いデータを指すインデックスを更新する
        m_nIndexGet = m_nLength == m_nSize ? m_nIndexAdd : 0;
    }

    // データを取得する
    String get(int index){
        // 引数のチェック
        if( m_nLength <= index ){
            System.err.println("Out of Range");
            System.exit(1);
        }

        return m_vect.at((m_nIndexGet + index) % m_nSize);
    }

    // 要素数を返す
    int getLength(){
        return m_nLength;
    }
}

C:\java>javac TailLine.java

C:\java>java TailLine
1
2
3
4
5
6
7
8
9
0
--end--
6
7
8
9
0

C:\java>


7章：演算子

12. まとめ

    これまでに変数やメソッドにつける修飾子がいくつか出てきましたが、ここで整理してみます。 なお、まだ登場していない修飾子も出てきますが、これについては後述します。

クラスの修飾子
修飾子 | 意　　味
public 他のどのクラスからでも使用できる
これをつけないと、同じパッケージ内のクラスのみ
abstract | インスタンス化(new)できないクラス(抽象クラス)
final | 継承できないクラス(サブクラスを作れないクラス)

メソッドの修飾子
修飾子 | 意　　味
public | 他のどのクラスからでも使用できる
protected | 同じパッケージ内のクラスもしくは、サブクラスからのみアクセスできる
private | 同じクラスからしかアクセスできない
上記３つのいずれでもない | 同じパッケージ内のクラスからのみアクセスできる
abstract | 実装しないメソッド(サブクラスで実装する必要がある)
final | オーバーライドできないメソッド
native | 他の言語で実装されるメソッド(本サイトでは解説しません)
static | 静的メソッド
synchronized | 実行するときにロックをするメソッド(後述)

変数の修飾子
修飾子 | 意　　味
public | 他のどのクラスからでも使用できる
protected | 同じパッケージ内のクラスもしくは、サブクラスからのみアクセスできる
private | 同じクラスからしかアクセスできない
上記３つのいずれでもない | 同じパッケージ内のクラスからのみアクセスできる
final | 定数(書き換えられない変数)
static | 静的変数
transient | クラスの永続的な状態の一部ではない変数(本サイトでは解説しません)
volatile | 不意に値が変更される可能性がある変数(後述)


8章：継承

1. 継承とは

    継承とは、あるクラス(A_Class)があるクラス(B_Class)の一種である場合、A_Classは B_Classを継承していると言います。そして、A_ClassはB_CLassの 子クラス、サブクラス、派生クラスと言い、逆にB_ClassはA_Classの 親クラス、スーパークラス、基底クラスと言います。

    これをJavaで定義すると以下のようになります。
class B_Class{
    :
    :
}

class A_Class extends B_Class
    :
    :
}

    「一種である」というのは、下記の図形の例のように「楕円」や「線」や「多角形」は 「図形」の一種であり、「円」は「楕円」の一種。「直線」は「線」の一種であるし、 「三角形」や「四角形」は「多角形」の一種であるということである。

    つまり、「三角形」や「四角形」は「多角形」を継承していると言い、「三角形」や「四角形」は 「多角形」の子クラス(サブクラス)であると言い、「多角形」は「三角形」や「四角形」の 親クラス(スーパークラス、基底クラス)であるという。

    また、「正方形」は「菱形」と「長方形」の両方の特別な場合であり、このような 継承の仕方を多重継承という。Javaのようにオブジェクト指向言語でも多重継承をサポート していない言語も多いが、C++では多重継承をサポートしている。しかし、あまり使われない。

    継承すると、親クラスのメンバー変数、メンバー関数は、その子クラスでも使用できるようになる。 子クラスでは親クラスのメンバー関数をオーバーライドすることができるため、 子クラスで実相をより高機能化することができる。


8章：継承

    いよいよ継承についてです。前章の練習問題で作った StringVectorクラスにソート機能を追加してみることにします。ソートなしのStringVectorクラスが あっても別に良いので、StringVectorクラスを改良してソート機能を追加するのではなく、 StringVectorクラスを継承して作ってみます。つまりソート機能付ベクタークラスは、 StringVectorクラスの一種であると考えるわけです。

    継承するには以下のように記述します。

public class SortableStringVector extends StringVector{
    :
    :
    :
}
    実際にソート機能付SortableStringVectorクラスを作るのは次のページ になります。

    余談ですが、私が継承を教えるときには、子クラスのことを「進化したクラス」と 教えています。「進化した」というのは一般的な言葉ではありませんが、色々な意味で 「進化した」という言葉があてはまるからです。子クラスのことを「進化した」と言っている 人がいたら、きっとその人は私に継承について教わった人か、その人から教わったか、 またはこのページを見た人でしょう。数年後に一般的に使われるようになっていたら 面白いですね。

    なぜ進化したクラスなのかと言いますと、

  • 親クラスよりも機能が上である
  • 親クラスから生まれたクラスである
だからです。


8章：継承

2. スーパークラスのコンストラクタ

    クラスを継承して子クラスを作る場合、そのほとんどが親クラスのコンストラクタを明示的に呼び出す必要があります。 つまり親クラスのデフォルトコンストラクタ以外を呼び出したい場合などです。

    StringVectorクラスと、ソート機能付Vectorクラス(SortableStringVectorクラス)の場合を考えてみます。 SortableStringVector クラスのコンストラクタに、配列の大きさを指定すると、その引数を親クラスのコンストラクタに通知しなければなりません。 その場合は「super()」を使います。このsuperは、コンストラクタの最初にしか用いることはできません。

    では、SortableVectorクラスを作ってみることにします。
Test.java

class Test{
    public static void main(String args[]){
        SortableStringVector vect = new SortableStringVector();
        String str;
        int    i = 0;
        while(true){
            str = readLine();
            if( str.length() == 0 )
                break;

            vect.at(i, str);
            i++;
        }

        vect.sort();

        int n = vect.getLength();
        for( i = 0 ; i < n ; i++ ){
            str = vect.at(i);
            if( str == null )
                break;
            System.out.print(str);
        }
    }

    // １行読み込んで、String型として返す
    public static String readLine(){
        byte b[] = new byte[1024];
        try{
            System.in.read(b);
        }catch(Exception e){
            return null;
        }

        int i;
        for( i = 0 ; b[i] != 0 && i < b.length ; i++ );
        return new String(b, 0, i);
    }
}

StringVector.java

public class StringVector{
    ////////////////////////////////////////////////////
    // 管理情報

    protected int m_size;            // 配列サイズ
    protected String m_vector[];     // 配列

    // コンストラクタ：引数あり
    // StringVector オブジェクトが定義された時に自動的に呼び出される
    public StringVector( int sz ){
        // 配列サイズを保存する
        m_size = sz ;

        // 配列の実体(size個のint配列)を自由記憶上に割り当てる
        m_vector = new String[m_size] ;
    }

    // コンストラクタ：引数なし
    // StringVector オブジェクトが定義された時に自動的に呼び出される
    public StringVector( ){
        // もう１つのコンストラクタを呼び出す
        this(100);
    }

    // 代入
    public void at( int n, String value ){
        if( n >= m_size || n < 0 ){
            System.err.println("invalid index");
            System.exit(1) ;        // 異常終了する
        }

        m_vector[n] = value ;
    }

    // 取得
    public String at(int n){
        if( n >= m_size || n < 0 ){
            System.err.println("invalid index");
            System.exit(1) ;        // 異常終了する
        }

        return m_vector[n];
    }

    // 現在の配列サイズを獲得する
    public int getLength(){
        return m_size;
    }
}

SortableStringVector.java

public class SortableStringVector extends StringVector{
    ////////////////////////////////////////////////////
    // 管理情報

    // コンストラクタ：引数あり
    // SortableStringVector オブジェクトが定義された時に自動的に呼び出される
    public SortableStringVector( int sz ){
        // 親クラスのコンストラクタ呼び出し
        super(sz);
    }

    // コンストラクタ：引数なし
    // SortableStringVector オブジェクトが定義された時に自動的に呼び出される
    public SortableStringVector( ){
        // もう１つのコンストラクタを呼び出す
        this(100);
    }

    // バブルソート
    void sort(){
        int j    = m_size - 1;
        int flag = m_size;
        for( ; j > 0 && flag != 0 ; j-- ){
            int i = 0;
            flag = 0;
            for( ; j > i ; i++ ){
                if( m_vector[i] != null && m_vector[i+1] != null && m_vector[i+1].compareTo(m_vector[i]) > 0 ){
                    String a = m_vector[i+1];
                    m_vector[i+1] = m_vector[i];
                    m_vector[i] = a;
                    flag = 1;
                }
            }
        }
    }
}

C:\java>javac Test.java

C:\java\java Test
123
234
345
222

345
234
222
123

C:\java>
    このように作成されたSortableStringVectorは、その親クラスであるStringVectorクラスのメソッドを 使用することもできるし、新たに加えたsortメソッドも利用できます。親クラスを進化させることができるのです。


8章：継承

3. 継承とキャスト

    次のようにAクラスがBクラスの親クラスだった場合、Aクラス用の変数にBクラスのインスタンスを 代入することができます。しかし、逆はエラーになります。犬は哺乳類ですが、哺乳類は犬ではないのと 同様です。

class A{
    :
    :
}

class B extends A{
    :
    :
}

class Test{
    public static void main(String arg[]){
        // これはOK
        A a = new B();

        // これはエラー
        B b = new A();
    }
}

    これまで、StringVectorクラスを作ってきましたが、何もStringクラスに特化させる必要はなかったのです。 次のようにすれば、Stringクラス以外のクラスもベクターとして管理することができるのです。

    JavaではすべてのクラスがObjectクラスの子クラスです。最初から用意されているStringクラスは もちろん、これまで作ってきたStringVectorクラスや、SortableStringVectorクラスも実はObjectクラスを 継承しているのです。

    したがって、下記のようにStringVectorクラスをObjectVectorクラスに書き換えたベクタークラスを 用いて、今までどおりStringVectorのように使うことができます。

Test.java

class Test{
    public static void main(String args[]){
        ObjectVector vect = new ObjectVector();
        vect.at(0, "HogeHoge");
        vect.at(1, "HonyoHonyo");

        System.out.println(vect.at(0));
        System.out.println(vect.at(1));
    }
}

ObjectVector.java

// StringVectorのStringをObjectに変更したベクタークラス
public class ObjectVector{
    ////////////////////////////////////////////////////
    // 管理情報

    protected int m_size;            // 配列サイズ
    protected Object m_vector[];     // 配列

    // コンストラクタ：引数あり
    // ObjectVector オブジェクトが定義された時に自動的に呼び出される
    public ObjectVector( int sz ){
        // 配列サイズを保存する
        m_size = sz ;

        // 配列の実体(size個のint配列)を自由記憶上に割り当てる
        m_vector = new Object[m_size] ;
    }

    // コンストラクタ：引数なし
    // ObjectVector オブジェクトが定義された時に自動的に呼び出される
    public ObjectVector( ){
        // もう１つのコンストラクタを呼び出す
        this(100);
    }

    // 代入
    public void at( int n, Object value ){
        if( n >= m_size || n < 0 ){
            System.err.println("invalid index");
            System.exit(1) ;        // 異常終了する
        }

        m_vector[n] = value ;
    }

    // 取得
    public Object at(int n){
        if( n >= m_size || n < 0 ){
            System.err.println("invalid index");
            System.exit(1) ;        // 異常終了する
        }

        return m_vector[n];
    }

    // 現在の配列サイズを獲得する
    public int getLength(){
        return m_size;
    }
}

    ところが、mainメソッドを下記のように書き換えるとコンパイルエラーが出てしまいます。
Test.java

class Test{
    public static void main(String args[]){
        ObjectVector vect = new ObjectVector();
        vect.at(0, "HogeHoge");
        vect.at(1, "HonyoHonyo");

        String str1 = vect.at(0);
        String str2 = vect.at(1);
    }
}

C:\java>javac Test.java
Test.java:7: Incompatible type for declaration. Explicit cast needed to convert
java.lang.Object to java.lang.String.
        String str1 = vect.at(0);
               ^
Test.java:8: Incompatible type for declaration. Explicit cast needed to convert
java.lang.Object to java.lang.String.
        String str2 = vect.at(1);
               ^
2 errors

C:\java>

    これは、ObjectVectorクラスのatメソッドの戻り値がObjectクラスだからです。前述したように StringクラスはObjectクラスの一種ですが、ObjectクラスはStringクラスの一種ではないため、 Objectクラスとして返された戻り値はStringクラスに代入できないのです。

    しかし、プログラムを見ればお分かりのとおり、実際に戻ってくるのはStringクラスのインスタンスです。 この場合は、下記のようにキャストします。
Test.java

class Test{
    public static void main(String args[]){
        ObjectVector vect = new ObjectVector();
        vect.at(0, "HogeHoge");
        vect.at(1, "HonyoHonyo");

        String str1 = (String)vect.at(0);
        String str2 = (String)vect.at(1);
    }
}

C:\java>javac Test.java

C:\java>

    では、Stringクラスのインスタンスが入っていないのに、Stringクラスにキャストしようとするとどうなるのでしょうか？ 次の例では、StringクラスではなくTestクラスのインスタンスをObjectVectorに入れて、Stringクラスにキャストして 取り出そうとしています。
Test.java

class Test{
    public static void main(String args[]){
        ObjectVector vect = new ObjectVector();
        vect.at(0, "HogeHoge");
        vect.at(1, new Test());

        String str1 = (String)vect.at(0);
        String str2 = (String)vect.at(1);
    }
}

C:\java>javac Test.java

C:\java>java Test
Exception in thread "main" java.lang.ClassCastException: Test
        at Test.main(Test.java:8)

C:\java>
    文法的には間違っていないのでコンパイルは正常にできますが、実際に実行しようとした場合に キャストできないとエラーになってしまいます。

    このようなことにならないようにプログラマーは気をつけなければいけませんが、下記のように instanceof演算子を用いて、ミスしても大丈夫なようにするべきです。
Test.java

class Test{
    public static void main(String args[]){
        ObjectVector vect = new ObjectVector();
        vect.at(0, "HogeHoge");
        vect.at(1, new Test());

        String str1 = null;
        String str2 = null;
        Object obj;

        obj = vect.at(0);
        if( obj instanceof String )
            str1 = (String)obj;

        obj = vect.at(1);
        if( obj instanceof String )
            str2 = (String)obj;
    }
}

C:\java>javac Test.java

C:\java>java Test

C:\java>


8章：継承

4. スコープ

    同じ変数や関数名を、親クラス、子クラスで定義するとどのように なるでしょうか。コンパイラは次のような順番で、目的とする変数や関数を検索します。

    派生クラスを作り、その派生クラスのメンバー関数での例を示します。
  • 派生クラスのメンバーメソッド内で定義した変数
  • 派生クラスのメンバー変数
  • 親クラスのメンバー変数

    実際のプログラムで例を示します。
// 親クラス
class Base{
    public    int a;
    private   int b;
}

// 子クラス
class Deriv extends Base{
    public  int b;
    private int c;

    public  void member_func_deriv(){
        a = 1;          // 派生クラスにはないので、親クラスのaを参照
        b = 2;          // 派生クラスのbを参照
        c = 3;          // 派生クラスのcを参照

        super.a = 10;   // Baseクラスのaを参照
//      super.b = 20;   // Baseクラスのbを参照しようとするが、privateなのでエラー

        int    a;
        double b;
        a = 100;        // メソッド内が優先される
        b = 1.1;        // 型が違っても大丈夫
    }

    public static void main(String args[]){
        Deriv d = new Deriv();

        d.a = 10;       // 派生クラスにはないので、親クラスのaを参照
        d.b = 10;       // 派生クラスのbを参照
        d.c = 10;       // 派生クラスのcを参照しようとするが、privateなのでエラー

//      d.super.a = 10; // このような使い方はできない
    }
}


8章：継承

5. クラスの作成

    派生クラスが作られた(newされた)場合は、JVMは以下のような操作を行う。

クラスの構築
  • 領域が確保される
  • 派生クラスの引数リストの一致するコンストラクタを呼び出す
  • superがあれば、親クラスの引数リストの一致するコンストラクタを呼び出す
  • superがなければ、親クラスの引数リストのないコンストラクタ(デフォルトコンストラクタ)を呼び出す
  • 派生クラスのコンストラクタの続きが実行される
※親クラスが他のクラスの派生クラスならば、再帰的に繰り返される

    前述した通り、superによる親クラスのコンストラクタ呼び出しは、 コンストラクタの１番最初で行わなければならない。子クラスは親クラスの進化した形であるため、親クラスの 初期化が行われていない(コンストラクタが呼ばれていない)状態で、より進化した形である子クラスを 初期化できないからです。言い方を変えると、子クラスが実行される時には親クラスの初期化が終わっていることが 保障されているということになります。

    実際のプログラムで例を示します。
// 親クラス
class Base{
    public Base(){
        System.out.println("Base::Base()");
    }
    public Base(int i){
        System.out.println("Base::Base(int)");
    }
}

// 子クラス
class Deriv extends Base{
    public Deriv(){
        System.out.println("Deriv::Deriv()");
    }
    public Deriv(int i){
        super(i);
        System.out.println("Deriv::Deriv(int)");
    }

    public static void main(String args[]){
        System.out.println("main");
        Deriv d1 = new Deriv();
        Deriv d2 = new Deriv(1);
    }
}

C:\java>javac Deriv.java

C:\java>java Deriv
main
Base::Base()
Deriv::Deriv()
Base::Base(int)
Deriv::Deriv(int)
C:\java>


8章：継承

6. abstarct

    abstractとはC++で言う純粋仮想関数です。メソッドの定義だけしておきそのクラスでは実装せずに、 子クラスで実装するためのものです。abstractを含むクラスは完成していないクラスとなるために、 インスタンスは作れません。子クラスでそのメソッドを実装し、子クラスのインスタンスを作ります。

    abstractなクラスは、「class」の前に「abstract」をつけないと、コンパイルエラーになってしまいます。

// 親クラス
abstract class Base{
    abstract public void func();
}

// 子クラス
class Deriv extends Base{
    public void func(){
        System.out.println("Deriv::func");
    }

    public void main(String args[]){
        Base  base1 = new Deriv();  // OK
        Base  base2 = new Base();   // エラー
    }
}

基底クラスに純粋仮想関数が複数ある場合、すべての純粋仮想関数を派生クラスで実装しなければその派生クラスもまた抽象クラスになる。
// 親クラス
abstract class Base{
    abstract public void func1();
    abstract public void func2();
}

// 子クラス
abstract class Deriv extends Base{
    public void func1(){
        System.out.println("Deriv::func1");
    }
}

// さらに子クラス
class DDeriv extends Deriv{
    public void func2(){
        System.out.println("DDeriv::func2");
    }
    public void main(String args[]){
        Base  base1 = new DDeriv();  // OK
        Base  base2 = new Deriv();   // エラー
        Base  base3 = new Base();    // エラー
    }
}

    抽象クラスは概念として導入する場合が多いです。つまりお絵かきソフトを作ろうと考えた場合、 図形データはObjectVectorクラスに入れます。図形データは以下のように作ります。
// 図形クラス(抽象クラス)
abstract class Zukei{
    protected int x1, x2, y1, y2;

    // 描画する関数
    public abstract void draw();

    // セーブする関数
    public abstract void save(String filename);
}

// 四角形クラス
class Rectangle extends Zukei{
    // 描画する関数(このクラスで実装する)
    public void draw(){
        :
        :
    }

    // セーブする関数(このクラスで実装する)
    public void save(String filename){
        :
        :
    }
}

// 直線クラス
class Line extends Zukei{
    // 描画する関数(このクラスで実装する)
    public void draw(){
        :
        :
    }

    // セーブする関数(このクラスで実装する)
    public void save(String filename){
        :
        :
    }
}

// 円クラス
class Circle extends Zukei{
    // 描画する関数(このクラスで実装する)
    public void draw(){
        :
        :
    }

    // セーブする関数(このクラスで実装する)
    public void save(String filename){
        :
        :
    }
}

class Draw{
    public static void main(String args[]){
        ObjectVector vect = new ObjectVector();
        vect.at(0, new Line());
        vect.at(1, new Circl());
    }
}


8章：継承

トップ-> Java入門：8章 継承-> 例題）お絵かきソフト

← 前ページへ　:　 トップへ　:　 次ページへ →
7. 例題）お絵かきソフト

    ソースコードはこちら (ZIP形式圧縮=約8Kbytes)

← 前ページへ　:　 トップへ　:　 次ページへ →


8章：継承

8. 継承と包含

新しいクラスを作成しようとした際には、次の３つの選択がある。
  1. まったくゼロから作る
  2. 既存のクラスを継承して、派生クラスとして作る
  3. 既存のクラスを包含して、新しく作る

    まったく新しい機能のクラスを作る場合は(1)のゼロから作る方法ですが、問題は (2)の継承か、(3)の包含かは、結構迷う場合もあります。プログラマー(設計者)の 経験に頼るところが大きいのですが、迷った場合は以下の５つの質問をしてみて、 すべての項目がYESだった場合は「継承」にします。どれか１つでもNOがあれば 「包含」にします。

    なお、質問事項の他にその解説と、お絵かきソフトの 例を示しました。

  • 既存のクラスの一種か(... kind of ...か)？
    文字どおり、基底クラスの一種ではないものは、派生クラスにするべきではありません。

    RectクラスやLineクラスはDiagramクラスの一種であるため、この質問はYESになります。

  • 機能の削除ではなく、機能の追加か？
    派生クラスは機能を追加するべきで、機能を削除して廉価版を作るべき ではありません。もし使い方を単純にして低機能なクラスを作るのであれば、低機能なクラスを 基底クラスにするべきです。もしくは、包含クラスとして実装する方法もあります。

    抽象クラスを派生して具象クラスを作ろうとしているので、YESになります。

  • 派生系列に、矛盾がないか？
    今までは分類で派生してきたのに、新しく派生したものは機能で分類していないか。 例えば、哺乳類クラスを派生して、犬クラス、猫クラスを作ったのに、さらに犬クラスを派生して 盲導犬クラスや警察犬クラスを作ろうとしていないか。(盲導犬や警察犬は種類ではなく、機能の追加になる。)

    １世代しか継承しないので、この質問はYESになります。

  • 既存のクラスの派生クラスにした場合、基底クラスやたの派生クラスに変更を加える必要はないか？
    他のクラスを変更するということは、それまで作ってきたプログラムが使えなくなるという ことです。他のクラスに変更したり、他のクラスが無用になるくらいなら、新しく派生クラスを つくるべきではありません。

    Rectクラス、Lineクラスが存在し、新たにCIrcleクラスを付け加える場合、どのクラスにも変更がないので、 この質問はYESになります。

  • わけのわからない(無用な)機能の追加ではないか？
    全然関係ない機能を付け加えるべきではありません。

    Diagramクラスは抽象クラスで、Rectクラスなどは、その具象クラスなので、この質問もYESになります。


9章：インターフェース

1. インターフェースとは

    ひし形クラスと長方形クラスの両方の特徴を継承した正方形クラスのようなものを多重継承と言います。 しかし、Javaでは多重継承の概念は取り入れられていません。これを解決するためにインターフェースという 機能をサポートしています。

    インターフェースとは、定数とメソッドの宣言だけを集めたものです。宣言だけで実際のメソッド本体などは 実装しません。あるクラスを作ろうとした場合に、「インターフェースをインプリメントする」と、そのクラスは インターフェースで宣言したメソッドと変数を必ず実装しなければならなくなります。

    つまり、MyInterfaceというインターフェースを作り、その中で「func1」というメソッドを宣言したとします。 そして、MyClassを作ろうとした場合にMyInterfaceをインプリメントした場合、MyClassの中には必ずfunc1という メソッドを実装する必要があるのです。

    一般に継承は系統を表し、インターフェースは機能を表します。しかし、具体的な機能を決めるものではなく、 このような機能があることを明示するためのものです。したがって、インターフェースはJavaのポリモーフィズム を実現する1つの手段であるといえます。

    ここで例を示します。犬クラスは哺乳類クラスのサブクラスです。犬にも秋田犬クラス、ゴールデンレトリバークラスや ブルドッグクラスなどがサブクラスとして存在するかもしれません。これらは動物の種類の系統でサブクラスを作って きています。また犬には、盲導犬や麻薬捜査犬、救助犬といった分類もありますが、これらはそれぞれ、 盲人を誘導する機能を有するとか、麻薬を見つけるとか、救助するといった機能を表します。 つまり、犬の種類とは関係ありません。したがって、これら機能（インタフェース）をインプリメントした犬クラスが 盲導犬クラスというように定義できるのです。

    今の世の中では盲導象や盲導猫というのは存在しませんが、もしかすると将来盲導猫というものが できたとすれば、猫クラスのサブクラスで、盲導インターフェースをインプリメントして盲導猫クラスを 作ることができます。


9章：インターフェース

2. インターフェースの宣言

interface interfaceName{
    type name = value                   // 変数
    type method( paramType name... );   // メソッド
}

例

interface Shape2D{
    // 面積を計算する
    double getArea();
}
    上記のようなShape2Dインターフェースをインプリメントしたクラスは、必ずgetAreaメソッドを実装しなければ なりません。下記にその使用例を示します。

// 図形クラス
abstract class Shape{
    abstract void Display();
}

// 円クラス
class Circle extends Shape implements Shape2D{
    int x;
    int y;
    int r;

    Circle(int x, int y, int r ){
        this.x = x;
        this.y = y;
        this.r = r;
    }

    void Display(){
        System.out.println("x=" + x + ", y=" + y + ", r=" + r);
    }

    // 面積を計算する
    public double getArea(){
        return r*r*Math.PI;    // Math.PI:円周率
    }
}

// 長方形クラス
class Rect extends Shape implements Shape2D{
    int x1;
    int y1;
    int x2;
    int y2;

    Rect(int x1, int y1, int x2, int y2 ){
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
    }

    void Display(){
        System.out.println("x1=" + x1 + ", y1=" + y1 + ", x2=" + y2 + ", y2=" + y2);
    }

    // 面積を計算する
    public double getArea(){
        // Math.abs : 絶対値を求めるメソッド
        return Math.abs(x1-x2) * Math.abs(y1-y2);
    }
}

    Shapeクラスは図形クラスであり、図形というからには何らかの形で表示する必要があります。表示方法は 円や三角形、長方形など図形によって異なります。そこで、Shapeクラスでは実装できずサブクラスで実装する 必要があるため、abstractになっています。

    Shapeクラスのサブクラスとして、ここではCircleクラス（円クラス）とRectクラス（長方形クラス）を 定義しています。ここでは、そのクラスの機能として、面積を計算するためのShape2Dインターフェースを インプリメントしています。


9章：インターフェース

3. インターフェースの参照

    では前ページのCircleクラスやRectクラスとインターフェースを使ってみます。

Test.java

public class Test{
    public static void main(String args[]){
        Shape2D array[] = new Shape2D[5];

        array[0] = new Circle(5,3,2);
        array[1] = new Rect(5,3,20,7);
        array[2] = new Circle(3,1,1);
        array[3] = new Rect(0,0,3,3);
        array[4] = new Circle(-5,2,2);

        double area = 0;    // 面積
        area += array[0].getArea();
        area += array[1].getArea();
        area += array[2].getArea();
        area += array[3].getArea();
        area += array[4].getArea();

        System.out.println("面積の合計＝"+area);
    }
}

C:\java>java Test
面積の合計＝97.27433388230814

C:\java>


9章：インターフェース

4. インターフェースと修飾子

    クラスにはpublic, protected, privateといった修飾子があることは前述しました。 インターフェースにもこれら修飾子を使うことも可能です。もちろん、インターフェースに含まれる変数や メソッドにも用いることも可能です。

public interface Shape2D{
    // 面積を計算する
    public double getArea();
}


9章：インターフェース

5. インターフェースの継承

    クラスには継承という機能があることは前述しました。 インターフェースにも継承という機能があります。クラスは多重継承ができませんが、インターフェースは 多重継承をすることができます。その場合、extendsの後に継承したいインターフェースをカンマで区切って 記述します。

public interface Interface extends Inteface1, Interface2{
    // 面積を計算する
    public double method();
}


9章：インターフェース

6. インターフェースとinstaceof演算子

    クラスはinstanceof演算子で、インスタンスがそのクラスのインスタンスか否かを判定することができる ことは前述しました。 　　インターフェースも同様にインスタンスが指定されたインターフェースをインプリメントかどうかを instanceof演算子を用いて調べることができます。

    下記の例は、面積の合計を求める先ほどのサンプルをinstanceof演算子を 用いて、円の面積の合計と長方形の面積の合計をそれぞれ算出するように書き換えたものです。

Test.java

public class Test{
    public static void main(String args[]){
        Shape2D array[] = new Shape2D[5];

        array[0] = new Circle(5,3,2);
        array[1] = new Rect(5,3,20,7);
        array[2] = new Circle(3,1,1);
        array[3] = new Rect(0,0,3,3);
        array[4] = new Circle(-5,2,2);

        double areaCircle = 0;   // 円の面積
        double areaRect   = 0;   // 長方形の面積

        for( int i = 0 ; i < array.length ; i++ ){
            if( array[i] instanceof Circle )
                areaCircle += array[i].getArea();
            else
                areaRect   += array[i].getArea();
        }

        System.out.println("円面積の合計＝"+areaCircle);
        System.out.println("長方形面積の合計＝"+areaRect);
        System.out.println("面積の合計＝"+ (areaCircle + areaRect));
    }
}

C:\java>java Test
円面積の合計＝28.274333882308138
長方形面積の合計＝69.0
面積の合計＝97.27433388230814

C:\java>


10章：パッケージ

1. パッケージとは

    パッケージとはクラスやインターフェースをひとまとめにしたものです。

    例えば、ファイルが多くなってくると同じカテゴリのファイルは新しいフォルダーを作成して そこに移動して、ファイルを整理しますよね。Javaでも同じようなことができます。つまり同じカテゴリの クラスやインターフェースは１つのパッケージにしてしまいます。

    ディレクトリと同じように、パッケージを階層構造にすることもできます。

    Javaでは標準で様々なクラスが用意されていますが、これらも機能のカテゴリごとに分類され、 パッケージ化されています。例えば、ネットワーク関係のクラスは「java.net」というパッケージに 各クラスがしまわれています。これは「java」パッケージの中の「net」パッケージという意味になります。 そのほかにも「java.util」、「java.lang」など多くのパッケージが用意されています。

パッケージ | 解説
java.lng | Javaの中核を成す機能を担うクラス群
java.util | ユーティリティー機能を提供するパッケージ
java.net | ネットワーク機能を提供するパッケージ
java.io | 入出力機能を提供するパッケージ
java.awt | Javaの基本的なGUI機能(AWT:Abstract Window Toolkit)を提供するパッケージ
java.awt.event | AWTの中でもイベントの機能（ボタンが押された場合など）を提供するパッケージ


10章：パッケージ

2. パッケージの宣言

    あるクラスをあるパッケージに格納するためにはいくつかの手続きが必要になります。 １つめはjavaファイルにパッケージ名を格納すること、２つめはパッケージ名と 同じディレクトリをつくり、そこにjavaファイルを格納することです。

1) Javaファイルへの記述

    Javaファイル(ソースファイル)の最初に

package packageName
と記述します。

    pakcageNameはパッケージ名をあらわし、例えば下記のように記述します。

// entertainmentパッケージとして定義
package entertainment

// entertainmentパッケージの中のTVパッケージとして定義
package entertainment.TV

2) ディレクトリ

    パッケージ構成と同じディレクトリを構成する必要があります。つまり上の例では、 entertainmentパッケージとしたjavaファイルは、「entertainment」というディレクトリの中に 格納する必要があり、entertainment.TVパッケージとしたjavaファイルは、「entertainment\TV」という ディレクトリの中に格納する必要があります。

3) パッケージの指定

    異なるパッケージのクラスを指定する場合は、「パッケージ名.クラス名」として参照します。 詳しくは下記の例を参照してください。

4) 例

PackageDemo.java

public class PackageDemo{
    public static void main(String args[]){
        // パッケージ内のクラスを参照する場合は「パッケージ名.クラス名」として参照する
        testPackage.A a = new testPackage.A();
        testPackage.B b = new testPackage.B();
        a.a1();
        b.b1();
    }
}

testPackage\A.java

// testPackageパッケージとして定義
package testPackage;

public class A{
    public void a1(){
        System.out.println("a1");
    }
}

testPackage\B.java

// testPackageパッケージとして定義
package testPackage;

public class B{
    public void b1(){
        System.out.println("b1");
    }
}

c:\java>javac PackageDemo.java

c:\java>java PackageDemo
a1
b1

c:\java>


10章：パッケージ

3. クラスパス

    「entertainment.TV」というクラスを指定した場合、javaは以下のような場所を探します。
  1. カレントディレクトリの中の「entertainment」ディレクトリの中のTV.class
  2. Javaが標準で用意されているクラス郡の「entertainment」パッケージの中のTV.class

    しかし、これだけでは、自分で部品として使えるようにしたクラス郡をパッケージとして作成した 場合や、便利なパッケージをダウンロードしてきた場合、いちいち自分がこれから作ろうとする ディレクトリにすべてコピーしなくてはいけなくなります。これは不便なので、一度部品として 切り分けられるようにして作ったパッケージは、ある特定のディレクトリにおいておき、 これを指定しておけば、いろいろなプログラムからそのパッケージをいつでも参照することができます。 その設定がクラスパスです。

    クラスパスはUNIXやWindowsでは環境変数に設定します。

c:\java>set classpath=c:\java\mypackage;c:\java\downloadpackage

c:\java>set classpath
CLASSPATH=c:\java\mypackage;c:\java\downloadpackage

c:\java>

このように設定すると、javaは

  1. カレントディレクトリの中の「entertainment」ディレクトリの中のTV.class
  2. c:\mypackage\entertainment\TV.class
  3. c:\downloadpackage\entertainment\TV.class
  4. Javaが標準で用意されているクラス郡の「entertainment」パッケージの中のTV.class
という順序でTV.classを探しに行きます。


10章：パッケージ

4. import

    前々ページの例では、testPackageパッケージ内のAクラスを指定するのに、 「testPackage.A」と記述しました。このように「パッケージ名.クラス名」と指定する方法を 完全修飾名を指定するといいます。

    しかしこの方法は、いちいちパッケージ名を指定しなければならないので、面倒です。 そこで、あらかじめ「testPackage」を使うよ！とJavacに指定してあげれば、いちいち 完全修飾名で指定しなくてもすみます。つまりC言語で言う「#include」のようなものです。 Javaでは「#include」ではなく「import」というステートメントを使用します。

    importはjavaファイルのはじめに記述し、そのjava内で使用するクラスをimportの後に、 「パッケージ名.クラス名」の形式で宣言します。そのパッケージ内のすべてのクラスを 宣言したい場合は、クラス名を「*」として指定します。また、複数のパッケージを指定することもでき、 その場合はimport行を何行も列挙します。

    以下に例を示します。左側は完全修飾名を指定して記述した場合で、右側はimportを使用して、 使用するパッケージを宣言した例です。以下の2つの例は同じになります。

PackageDemo.java

public class PackageDemo{
    public static void main(String args[]){
        // 完全修飾名
        testPackage.A a = new testPackage.A();
        testPackage.B b = new testPackage.B();
        a.a1();
        b.b1();
    }
}

PackageDemo.java

import testPackage.*;

public class PackageDemo{
    public static void main(String args[]){
        // 完全修飾名
        A a = new A();
        B b = new B();
        a.a1();
        b.b1();
    }
}

testPackage\A.java

// testPackageパッケージとして定義
package testPackage;

public class A{
    public void a1(){
        System.out.println("a1");
    }
}

testPackage\B.java

// testPackageパッケージとして定義
package testPackage;

public class B{
    public void b1(){
        System.out.println("b1");
    }
}

c:\java>javac PackageDemo.java

c:\java>java PackageDemo
a1
b1

c:\java>


11章：例外

1. 例外とは

    例外とは予期しなかった処理のことを言います。例えば、配列の中身の平均値を求めるメソッドを 作ろうとした場合、下記のようになると思います。

public class Test{
    public static void main(String args[]){
        int array[] = new int[5];

        array[0] = 10;
        array[1] =  5;
        array[2] =  2;
        array[3] =  4;
        array[4] =  9;

        System.out.println("average="+average(array));
    }

    public static int average(int[] array){
        int total = 0;
        for( int i = 0 ; i < array.length ; i++ )
            total += array[i];

        return total / array.length;
    }
}

C:\java>javac Test.java

C:\java>java Test
average=6

C:\java>
    ところが、配列の長さが0だった場合はaverageメソッドの中で「0で割る」という計算が発生してしまい、 エラーになってしまいます。

public class Test{
    public static void main(String args[]){
        // int array[] = new int[5];
        int array[] = new int[0];
/*
        array[0] = 10;
        array[1] =  5;
        array[2] =  2;
        array[3] =  4;
        array[4] =  9;
*/
        System.out.println("average="+average(array));
    }

    public static int average(int[] array){
        int total = 0;
        for( int i = 0 ; i < array.length ; i++ )
            total += array[i];

        return total / array.length;
    }
}

C:\java>javac Test.java

C:\java>java Test
Exception in thread "main" java.lang.ArithmeticException: / by zero
        at Test.average(Test.java:20)
        at Test.main(Test.java:12)

C:\java>
javaのバージョンなどにより、メッセージが異なる場合があります
    このように予期せぬエラーが発生するとJavaではそのスレッドが終了してしまいます。 この例のように単一スレッドの場合はプログラムが終了してしまいます。(スレッドについては 後述します。)

    このエラーメッセージにある「Exception」というのが例外をあらわしており、「in thread "main"」が スレッドを表しています。そしてその後の「java.lang.ArithmeticException: / by zero」が例外の内容を あらわしていて、ここではゼロで割ったという例外であることがわかります。次の行からどの部分で 例がが起こったかをあらわしています。Test.javaの20行目、Testクラスのaverageメソッドで起こっていることがわかり、 さらに次の行を見ると、Test.averageメソッドはTest.javaの12行目(Testクラスのmainメソッド)から呼び出されている ことがわかります。


11章：例外

2. 例外が起きたら

    例外が起きた場合に前ページの例のように、アプリケーションが終了してしまっては困ります。 そこでプログラマーは例外を予測して、例外が起きた場合に適当な処理をさせる必要があります。

    例えば、前ページの平均を求めるメソッドの場合、引数で与えられた配列が newし忘れたなどで、配列の実体が無かった場合などは、NullPointerExceptionという名前の例外が発生します。 　　前ページの平均を求めるメソッドでは、この2つの例外が発生する可能性が ありますので、例外が起きた場合専用の処理をするようにします。

public class Test{
    public static void main(String args[]){
        // int array[] = new int[5];
        int array[] = new int[0];

        System.out.println("average="+average(array));
    }

    public static int average(int[] array){
        try{
            int total = 0;
            for( int i = 0 ; i < array.length ; i++ )
                total += array[i];

            return total / array.length;
        }catch(NullPointerException e){
            System.out.println("newし忘れてませんか？");
        }catch(ArithmeticException e){
            System.out.println("配列が空です。");
        }
        return 0;
    }
}

C:\java>javac Test.java

C:\java>java Test
配列が空です
average=0

C:\java>
    上記の例のように、例外が発生すると思われる部分を
try{
    // 例外が発生する可能性のあるブロック
}catch(例外の種類1 param1){
    // 例外の種類1が発生した場合に実行される部分
}catch(例外の種類2 param1){
    // 例外の種類2が発生した場合に実行される部分
}finally{
    // 例外が起きても起きなくても実行される部分
}
    上記の構文を用いた場合、JVMはtryブロックを実行します。もしtryブロックの中で例外が発生しなければ、 catchブロックは実行せずに、finallyブロックを実行します。

    もし、tryブロック実行中に例外が発生したら、発生した場所から例外の内容に応じてcatchブロックが実行されます。 この場合、tryブロックはすべてが実行されるわけではなく、例外が発生した部分よりも下の部分は実行されずに catchブロックにジャンプします。上記の例で、averageメソッドの引数にnullが渡された場合、for分の array.lengthでNullPointerExceptionという例外が発生します。したがって、forの中身は実行されずに NullPointerExceptionをキャッチしているcatchブロックに処理が移行します。

    なお、finallyブロックは省略することができます。catchブロックは１つ以上必要になります。


11章：例外

3. 例外の種類

    例外にはいろいろ種類があります。下表に代表的な例外を紹介します。
例外 | 内容
ClassNotFoundException | 呼び出そうとしたクラスのクラスファイルが無かった場合
CloneNotSupportedException | オブジェクトを複製するために Object クラスの clone メソッドが呼び出されたが、そのオブジェクトのクラスが Cloneable インタフェースを実装していないことを示すためにスローされます。
DataFormatException | データ形式エラーが発生したことを通知します。
IOException なんらかの入出力例外の発生を通知するシグナルを発生させます。
サブクラスとしてEOFException, FileNotFoundExceptionなどがあります
PrinterException 印刷システムに例外的な状態が発生したことを示すために使用されます。
サブクラスとしてPrinterAbortException, PrinterIOExceptionがあります。
RuntimeException | JVMの通常の処理でスローすることができる各種の例外のスーパークラスです。
ArithmeticException RuntimeExceptionのサブクラスです。
算術計算で例外的条件が発生した場合にスローされます。たとえば、「ゼロで除算される」整数はこのクラスのインスタンスをスローします。
ArrayStoreException RuntimeExceptionのサブクラスです。
不正な型のオブジェクトをオブジェクトの配列に格納しようとしたことを示すためにスローされます。
ClassCastException RuntimeExceptionのサブクラスです。
あるオブジェクトを継承関係にないクラスにキャストしようとしたことを示すためにスローされます。
IllegalArgumentException RuntimeExceptionのサブクラスです。
不正な引数、または不適切な引数をメソッドに渡したことを示すためにスローされます。
IndexOutOfBoundsException RuntimeExceptionのサブクラスです。
ある種のインデックス (配列、文字列、ベクトルなど) が範囲外であることを示すためにスローされます。
サブクラスにArrayIndexOutOfBoundsException, StringIndexOutOfBoundsExceptionがあります。
NegativeArraySizeException RuntimeExceptionのサブクラスです。
負のサイズを持った配列をアプリケーションが作成しようとした場合にスローされます。
NoSuchElementException RuntimeExceptionのサブクラスです。
この列挙にそれ以上の要素がないと、Enumeration の nextElement メソッドによってスローされます。
NullPointerException RuntimeExceptionのサブクラスです。
オブジェクトが必要な場合に、アプリケーションが null を使おうとするとスローされます。
SecurityException RuntimeExceptionのサブクラスです。
セキュリティマネージャによってスローされ、セキュリティ違反を示します。
サブクラスにAccessControlException, RMISecurityExceptionがあります。


11章：例外

4. catchブロックの検索

    tryブロック内で例外が発生した場合は、tryブロックの次に記述されているcatchブロックに対応する 例外処理が書かれてるかを検索します。しかし、対応する例外処理が書かれていなければ(対応する例外を キャッチするcatchブロックが無い場合は)、そのtryブロックを囲っているさらに上のtryブロックに書かれている ctachブロックを検索します。つまり、例外処理が起きたメソッドを呼び出している呼び出し元のtry～catch ブロックなどを探します。そこにも発生した例外をキャッチするcatchブロックが無ければ、さらに呼び出し元を 検索します。

    可能な限りさかのぼってもキャッチされない場合は、そのスレッドはJVMによって強制的に終了されて しまいます。もし単一スレッドであった場合は、アプリケーションが終了します。

public class Test{
    public static void main(String args[]){
        try{
            a();
        }catch(NullPointerException e){
            System.err.println(e);
        }
        System.out.println("アプリケーションは正常に終了しました");
    }

    public static void a(){
        try{
            b();
        }catch(ArithmeticException e){
            System.err.println(e);
        }
    }

    public static void b(){
        int array[] = new int[2];
        // ArrayIndexOutOfBoundsExceptionを発生
        array[3] = 10;
    }
}

C:\java>javac Test.java

C:\java>java Test
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
        at Test.b(Test.java:22)
        at Test.a(Test.java:12)
        at Test.main(Test.java:3)

C:\java>
    上記の例では、bメソッドでArrayIndexOutOfBoundsExceptionが発生しています。ところが、bメソッドには catchブロックが無いため、bメソッドを呼び出しているaメソッドを見ます。aメソッドはtry～catchブロック がありますので、catchブロックを見ますが、発生した例外をキャッチはしていません。そこでさらにaメソッドを 呼び出しているmainメソッドを見ます。ところがmainメソッドもtry～catchはしているものの、発生した例外を キャッチしていないため、アプリケーションが強制的に終了してしまいます。


11章：例外

    例外もクラスの1つで、継承関係があることは前述しました。継承関係が ある例外をキャッチする場合には、その記述に順番があります。キャッチブロックは上から順に検索されます。 さらに1度の例外に対して1度しかキャッチブロックは処理されないため、サブクラスから記述しなければなりません。

    例えば、NullPointerExceptionはRuntimeExceptionのサブクラスで、RuntimeExceptionはException のサブクラスですので、下記のように記述します。

public class Test{
    public static void main(String args[]){
        try{
            int array[] = null;
            array[0] = 10;
        }catch(NullPointerException e){
            System.err.println("NullPointerException");
        }catch(RuntimeException e){
            System.err.println("RuntimeException");
        }catch(Exception e){
            System.err.println("Exception");
        }
    }
}

C:\java>javac Test.java

C:\java>java Test
NullPointerException

C:\java>
    ところが下記の例では、スーパークラスのExceptionから記述しようとしたため、すべての例外は 最初のExceptionクラスのcatchブロックでキャッチされてしまいます。キャッチブロックは1回の例外で 1度しか処理しないため、その下のRuntimeExceptionやNullPointerExceptionではキャッチされません。 したがって、RuntimeExceptionやNullPointerExceptionのキャッチブロックは処理されることがないため、 コンパイルエラーになります。

public class Test{
    public static void main(String args[]){
        try{
            int array[] = null;
            array[0] = 10;
        }catch(Exception e){
            System.err.println("Exception");
        }catch(RuntimeException e){
            System.err.println("RuntimeException");
        }catch(NullPointerException e){
            System.err.println("NullPointerException");
        }
    }
}

C:\java>javac Test.java
Test.java:8: catch not reached.
        }catch(RuntimeException e){
         ^
Test.java:10: catch not reached.
        }catch(NullPointerException e){
         ^
2 errors

C:\java>


11章：例外

5. throw

    例外は受け取るだけでなく、呼び出し元に例外処理を任せることもできます。これを「例外を投げる」と言います。 例えば、そのキャッチブロックで例外処理をしただけではダメで、呼び出しもとのキャッチブロックでも 例外処理をしなければならないような場合に使います。

    下記の例ではaメソッド内で例外処理をした後、throwステートメントを用いて、例外を上に投げています。

public class Test{
    public static void main(String args[]){
        try{
            a();
        }catch(RuntimeException e){
            System.err.println("mainメソッド内で例外を処理をしました("+e+")");
        }
        System.out.println("アプリケーションは正常に終了しました");
    }

    public static void a(){
        try{
            int array[] = new int[2];
            // ArrayIndexOutOfBoundsExceptionを発生
            array[3] = 10;
        }catch(RuntimeException e){
            System.err.println("aメソッド内で例外を処理をしました("+e+")");
            throw e;
        }
    }
}

C:\java>javac Test.java

C:\java>java Test
aメソッド内で例外を処理をしました(java.lang.ArrayIndexOutOfBoundsException)
mainメソッド内で例外を処理をしました(java.lang.ArrayIndexOutOfBoundsException)
アプリケーションは正常に終了しました

C:\java>
    上記の例では、生成されたRuntimeExceptionのインスタンスをthrowで上に投げています。throwステートメントは 生成されたExceptinを投げるだけなく、プログラマーが明示的にnewした例外などを上に投げることもできます。 投げることができるのは、Throwableクラスのサブクラスだけです。Stringクラスなどのインスタンスは投げることが できません。ThrowableクラスのサブクラスはExceptionクラス(すべての例外のスーパークラス)と、 Errorクラスです。

public class Test{
    public static void main(String args[]){
        int array[] = new int[2];
        try{
            a(array, 10);
        }catch(RuntimeException e){
            System.err.println("mainメソッド内で例外を処理をしました("+e+")");
        }
    }

    public static int a(int array[],  int i){
        if( i < 0 || i >= array.length ){
            System.out.println("ArrayIndexOutOfBoundsExceptionを投げます");
            throw new ArrayIndexOutOfBoundsException();
        }

        System.out.println("aメソッドは正常に終了します");
        return array[i];
    }
}

C:\java>javac Test.java

C:\java>java Test
ArrayIndexOutOfBoundsExceptionを投げます
mainメソッド内で例外を処理をしました(java.lang.ArrayIndexOutOfBoundsException)

C:\java>


11章：例外

6. throws

    前ページの例のように、例外を投げる可能性のあるメソッドを作成した場合は、 他のプログラマーが見てもすぐに例外が投げられる可能性があることがすぐわかるように、明示しておくべきです。 明示するためには、例外を投げる可能性があるコンストラクタやメソッドの宣言時にthrowsステートメントを 用います。

public class Test{
    public static void main(String args[]){
        int array[] = new int[2];
        try{
            a(array, 10);
        }catch(RuntimeException e){
            System.err.println("mainメソッド内で例外を処理をしました("+e+")");
        }
    }

    public static int a(int array[],  int i) throws ArrayIndexOutOfBoundsException{
        if( i < 0 || i >= array.length ){
            System.out.println("ArrayIndexOutOfBoundsExceptionを投げます");
            throw new ArrayIndexOutOfBoundsException();
        }

        System.out.println("aメソッドは正常に終了します");
        return array[i];
    }
}

C:\java>javac Test.java

C:\java>java Test
ArrayIndexOutOfBoundsExceptionを投げます
mainメソッド内で例外を処理をしました(java.lang.ArrayIndexOutOfBoundsException)

C:\java>
    投げる例外が複数種ある場合は、カンマ区切りで列挙します。

    throwsステートメントは記述しなくても問題はありませんが、特に複数人でプログラミングする場合など、 自分以外の人にわかりやすくするために記述することが好ましい。また、1人でプログラムしていても、 しばらくたつと忘れてしまう場合もあるため、記述することをお勧めします。


11章：例外

7. エラーと例外

    throwできるクラスはThrowableクラスのサブクラスだけです。ThrowableクラスにはErrorクラスとException クラスの２つがあります。一般にアプリケーションの使い方をユーザーが間違えた場合、ファイルをセーブしようとしたのに ディスクがいっぱい、通信失敗など、一般に予期することができるようなエラーはJavaではエラーではなく 例外として処理します。逆にJVMの重大なエラーを示します。例えばメモリー不足、スタックオーバー、 内部エラー、不正アクセスなど、プログラマーやユーザーのせいではないエラーをエラークラスとします。 したがって、一般にはエラークラスのサブクラスを作成することはできますが、一般にはしません。

    Throwableクラス、Errorクラス、Exceptionクラスのコンストラクタとメソッドを紹介します。 これらのサブクラスを作る方法は次ページで紹介しますが、必要な メソッドをオーバーライドする必要があります。

Throwableクラスのコンストラクタの概要
Throwable()
エラーメッセージ文字列として null を指定して Throwable を新しく構築します。
Throwable(String message)
指定されたエラーメッセージを持つ Throwable を新しく構築します。
Throwableクラスのメソッドの概要
 Throwable fillInStackTrace()
実行スタックトレースを埋め込みます。
 String getLocalizedMessage()
Throwable の地域対応された記述を作成します。
 String getMessage()
この Throwable オブジェクトのエラーメッセージ文字列を返します。
 void printStackTrace()
Throwable とそのバックトレースを標準エラーストリームに出力します。
 void printStackTrace(PrintStream s)
Throwable とそのバックトレースを指定された印刷ストリームに出力します。
 void printStackTrace(PrintWriter s)
この Throwable とそのバックトレースを指定されたプリントライターに出力します。
 String toString()
Throwable オブジェクトの短い記述を返します。

Errorクラスのコンストラクタの概要
Error()
詳細メッセージを指定しないで Error を構築します。
Error(String s)
指定された詳細メッセージを持つ Error を構築します。

Exceptionクラスのコンストラクタの概要
Exception()
詳細メッセージを指定しないで Exception を構築します。
Exception(String s)
指定された詳細メッセージを持つ Exception を構築します。

Throwable, Error, Exception

public Throwable()
public Error()
public Exception()
エラーメッセージ文字列として null を指定して Throwable, Error, Exceotion を新しく構築します。

Throwable, Error, Exception

public Throwable(String message)
public Error(String message)
public Exception(String message)
指定されたエラーメッセージを持つ Throwable, Error, Exceotion を新しく構築します。
パラメータ:
message - エラーメッセージ。エラーメッセージは getMessage() メソッドによる取り出しのために保存される

getMessage

public String getMessage()
この Throwable オブジェクトのエラーメッセージ文字列を返します。
戻り値:
エラーメッセージ文字列付きで作成された場合は、この Throwable, Error, Exceptionオブジェクトのエラーメッセージ文字列。エラーメッセージなしで作成された場合は null

toString

public String toString()
Throwable オブジェクトの短い記述を返します。この Throwable, Error, Exception オブジェクトがエラーメッセージ文字列付きで作成された場合、結果は次の 3 つの文字列を連結したものになります。
  • このオブジェクト実際のクラス名
  • ": " (コロンとスペース)
  • このオブジェクトに対する getMessage() メソッドの結果
Throwable, Error, Exception オブジェクトがエラーメッセージ文字列なしで作成された場合は、オブジェクトの実際のクラス名が返されます。
戻り値:
この Throwable, Error, Exception の文字列表現
オーバーライド:
クラス Object内のtoString

printStackTrace

public void printStackTrace()
Throwable, Error, Exception とそのバックトレースを標準エラーストリームに出力します。
java.lang.ArithmeticException: / by zero
        at Test.average(Test.java:20)
        at Test.main(Test.java:12)


11章：例外

8. 独自の例外

    Exceptionクラスを継承して独自の例外クラスを作成することもできます。これはアプリケーション特有の 例外を作成したい場合などに使用されます。

    独自のエラークラスを投げるメソッドでは、必ずキャッチするか、throwsステートメント で、独自の例外を投げる可能性があることを明示しなければなりません。

// 独自の例外クラス
class MyException extends Exception{
    public MyException(){
        super();
    }
}

public class Test{
    public static void main(String args[]){
        try{
            a();
        }catch(MyException e){
            System.err.println("mainメソッド内で例外を処理をしました("+e+")");
        }
    }

    public static void a() throws MyException{
        throw new MyException();
    }
}

C:\java>javac Test.java

C:\java>java Test
mainメソッド内で例外を処理をしました(MyException)

C:\java>

    上記の例で、throws MyExceptionやmainメソッド内のcatchを忘れると、コンパイルエラーになります。

    ただし、独自の例外がRuntimeExceptionのサブクラスの場合は、この制限は適用されません。

// 独自の例外クラス
class MyException extends Exception{
    public MyException(){
        super();
    }
}
public class Test{
    public static void main(String args[]){
        try{
            a();
        }catch(MyException e){
            System.err.println("e");
        }
    }

    public static void a(){
        throw new MyException();
    }
}

// 独自の例外クラス
class MyException extends RuntimeException{
    public MyException(){
        super();
    }
}
public class Test{
    public static void main(String args[]){
        try{
            a();
        }catch(MyException e){
            System.err.println(e);
        }
    }

    public static void a(){
        throw new MyException();
    }
}

C:\java>javac Test.java
Test.java:11: Exception MyException is never thrown
in the body of the corresponding try statement.
        }catch(MyException e){
         ^
Test.java:17: Exception MyException must be caught,
or it must be declared in the throws clause of this method.
        throw new MyException();
        ^
2 errors

C:\java>

C:\java>javac Test.java

C:\java>


12章：マルチスレッド

1. スレッドとは

    今あなたがWindowsやMac OSを使っているとします。例えばエクセルのアイコンをクリックすると ハードディスクがカラカラ回って、Excel2000とかっていう絵(スプラッシュとか言います)が一瞬出て エクセルが起動します。このように１つのソフトを処理する単位をプロセスと言います。予断になり ますが、Windows XPで、Ctrl+Alt+Deleteを押してタスクマネージャーを表示すると、プロセスという タブが出ますので、選択しますと、現在どのくらいのプロセスが起動しているかわかります。

    １つのプロセスでは同時に複数の処理をすることができます。例えばInternet Explorerや Netscape Navigatorなどのブラウザでは、インターネットからファイルを取得する仕事が標準で ４つ動き、さらにユーザーからの操作(中止が押されたり、他のページにジャンプしたりなど)を受付る 仕事があり、これらが同時に動いています。この仕事1つ1つをスレッドと言います。

    複数のスレッドが同時に処理することができるプログラムをマルチスレッドプログラムと言います。 当然ながらWindowsやMac OSなどはマルチスレッドです。


12章：マルチスレッド

2. スレッドのライフサイクル

    実は同時にスレッドが動くといっても、普通のパソコンはCPUが１つなので、本当の意味で同時に 複数のスレッドが動いているわけではありません。１つめのスレッドがちょっと動いたら、次に２つめの スレッドがちょっと動き、さらに1つ目のスレッドが・・・。というように人間がわからないくらいの 短時間の間に交互に動くのです。これをタイムディビジョン（時分割）方式と言います。他にもいろいろな 方式がありますが、詳細は省略します。

    ちょっと専門的な話になりますが、スレッドには「実行状態」、「待機状態」、「実行可能状態」の ３つの状態があり、その関係は数のようになっています。

実行状態
実行状態とは、まさに今、実行されているスレッドです。

待機状態
待機状態とは、通信の返事が帰ってくるまで待つとか、キーボードの入力待ちなどイベントを 待ったり、イベントが終了するまで待っている状態。もしくは何もすることが無い状態です。

実行可能状態
実行可能状態とは、待機状態でイベントが終了し、いつでもスレッドが実行状態に 移れる状態を言います。本当ならばすぐに実行になりたいのですが、別のスレッドが実行中の場合は、この実行可能 状態で一時待つ感じになります。

    新しいスレッドができた場合は、実行可能状態になり、実行されるのを待ちます。今実行されているスレッドが 切がよくなったところで、作られたスレッドは実行中になります。あまり長い間実行していると他のスレッドが動けなく なるので、しばらくすると実行可能状態に戻されます。もしくはキーボードからの入力待ちなどがあると待機状態に なります。待機状態でイベントが終了すると実行可能状態になりますが、直接実行中状態になることはありません。 スレッドが破棄されるのは、基本的には実行中からそのスレッドの仕事が終わった時に破棄されます。基本的には というのは、OS(この場合JVM)や他のスレッドから強制的に削除されるような特殊な場合は待機状態などから 破棄されることもあります。

    このことは実際にプログラムする時にはあまり意識しなくても作れますが、情報処理技術者試験には まず出題されますので、試験を受ける方はきちんと覚えておいてください。また、この状態遷移図は Javaに限らず、ほかのOSでも同様のことが言えます。

    WindowsやMacなどでは、先に説明しました時分割の機能がありますが、Javaは他のプラットフォームでも 動きます。しかし他のプラットフォームのCPUが時分割をサポートしているとは限りません。この場合、 スレッド優先順位が高いスレッドが終了するまで優先順位の低いスレッドは動けなくなる場合がありますので、 注意が必要です。


12章：マルチスレッド

3. スレッドの作成(Threadクラス)

    スレッドを作るには、java.langパッケージのThreadクラスを継承すれば作れます。さらに、スレッドの 中身はrunメソッドをオーバーライドして記述します。そして、スレッドクラスのstartメソッドを呼び出せば、 新しいスレッドが作成され、runメソッドが別スレッドとして実行されます。

Test.java

// 独自のスレッドクラス
class MyThread extends Thread{
    public MyThread(){
    }

    // このrunメソッドがスレッド本体
    public void run(){
        System.out.println("MyThread Start!");

        // 10回500ミリ秒毎に"Hello"と表示する
        for(int i = 0 ; i < 10 ; i++ ){
            System.out.println("Hello");
            try{
                // sleepメソッドはThreadクラスの静的メソッドで、
                // nミリ秒(この場合500ミリ秒)待つ(待機状態になる)メソッド。
                // 例外を投げる可能性があるので、キャッチしないといけない
                Thread.sleep(500);
            }catch(Exception e){}
        }

        System.out.println("MyThread End!");
    }
}

public class Test{
    public static void main(String args[]){
        // 独自のスレッドクラスのインスタンスを作成
        MyThread t = new MyThread("Thread1");

        // スレッドを開始
        t.start();

        // 1秒毎に"Hi!"と10回表示する
        for(int i = 0 ; i < 10 ; i++ ){
            System.out.println("Hi!");
            try{
                Thread.sleep(1000);
            }catch(Exception e){}
        }
    }
}

C:\java>javac Test.java

C:\java>java Test
Hi!
MyThread Start!
Hello
Hello
Hi!
Hello
Hello
Hi!
Hello
Hello
Hi!
Hello
Hello
Hi!
Hello
Hello
Hi!
MyThread End!
Hi!
Hi!
Hi!
Hi!

C:\java>

    上の例のように、mainメソッドと同時に、runメソッドも動いていることがわかります。

※実行環境によって結果は若干変わる場合があります。


12章：マルチスレッド

4. Threadクラスのメソッド

    下表は、Java APIを引用したものです。詳細は省略していますので、ダウンロードしたJava APIもしくは、 SUNのホームページの最新かつ詳細なデータを参照してください。

    なお、黄色の部分は良く(?)使うものです。

フィールドの概要
static int MAX_PRIORITY
スレッドに設定できる最高優先順位です。
static int MIN_PRIORITY
スレッドに設定できる最低優先順位です。
static int NORM_PRIORITY
スレッドに割り当てられるデフォルトの優先順位です。
コンストラクタの概要
Thread()
新しい Thread オブジェクトを割り当てます。
Thread(Runnable target)
新しい Thread オブジェクトを割り当てます。
Thread(Runnable target,String name)
nameという名前の新しい Thread オブジェクトを割り当てます。
Thread(String name)
nameという名前の新しい Threadオブジェクトを割り当てます。
Thread(ThreadGroup group,Runnable target)
groupに新しい Thread オブジェクトを割り当てます。
Thread(ThreadGroup group,Runnable target,String name)
groupにnameという名前のその実行オブジェクトとして target 、名前として指定された name を持つ、group によって参照されるスレッドグループに属するような、新しい Thread オブジェクトを割り当てます。
Thread(ThreadGroup group,String name)
groupにnameという名前の新しい Threadオブジェクトを割り当てます。
メソッドの概要
static int activeCount()
このスレッドグループの現在アクティブなスレッドの数を返します。
 void checkAccess()
現在実行中のスレッドが、このスレッドを変更するためのアクセス権を持っているかどうかを判定します。
 int countStackFrames()
推奨されません。 この呼び出しの定義は、推奨されない suspend() に依存します。また、この呼び出しの結果は保証されません。
static Thread currentThread()
現在実行中のスレッドオブジェクトの参照を返します。
 void destroy()
クリーンアップを行わないでこのスレッドを破棄します。
static void dumpStack()
現在のスレッドのスタックトレースを出力します。
static int enumerate(Thread[] tarray)
このスレッドのスレッドグループおよびその下位グループ内のすべてのアクティブなスレッドを、指定された配列にコピーします。
 ClassLoader getContextClassLoader()
この Thread のコンテキスト ClassLoader を返します。
 String getName()
このスレッドの名前を返します。
 int getPriority()
このスレッドの優先順位を返します。
 ThreadGroup getThreadGroup()
このスレッドが所属するスレッドグループを返します。
 void interrupt()
このスレッドに割り込みます。
static boolean interrupted()
現在のスレッドが割り込まれているどうかを調べします。
 boolean isAlive()
このスレッドが生存しているかどうかを判定します。
 boolean isDaemon()
このスレッドがデーモンスレッドであるかどうかを判定します。
 boolean isInterrupted()
このスレッドが割り込まれているどうかを調べます。
 void join()
このスレッドが終了するのを待機します。
 void join(long millis)
このスレッドが終了するのを、最高で millis ミリ秒待機します。
 void join(long millis,int nanos)
最高で millis ミリ秒に nanos ナノ秒を加算した間、このスレッドが終了するのを待機します。
 void resume()
推奨されません。 このメソッドはデッドロックを発生しやすいため推奨されません。suspend() とともに使用するためだけに提供されています。
 void run()
このスレッドが別個の Runnable 実行オブジェクトを使用して作成された場合、その Runnable オブジェクトの run メソッドが呼び出されます。
 void setContextClassLoader(ClassLoader cl)
この Thread のコンテキスト ClassLoader を設定します。
 void setDaemon(boolean on)
このスレッドを、デーモンスレッドまたはユーザスレッドとしてマークします。
 void setName(String name)
このスレッドの名前を引数 name に等しくなるように変更します。
 void setPriority(int newPriority)
このスレッドの優先順位を変更します。
static void sleep(long millis)
現在実行中のスレッドを、指定されたミリ秒数の間、スリープ (一時的に実行を停止) させます。
static void sleep(long millis,int nanos)
現在実行中のスレッドを、指定されたミリ秒数に指定されたナノ秒数を加算した間、スリープ (実行停止) させます。
 void start()
このスレッドの実行を開始します。
 void stop()
推奨されません。 このメソッドは本質的に安全ではありません。Thread.stop を使ってスレッドを停止すると、チェックされていない ThreadDeath 例外がスタックの下から上に伝えられた結果、そのスレッドによりロックされていたモニターのロックがすべて解除されます。これらのモニターによって直前まで保護されていたオブジェクトが整合性のない状態にあった場合、壊れたオブジェクトは他のスレッドに対して可視になり、動作が保証されないことがあります。多くの場合、ターゲットスレッドの実行停止を指示するには、stop ではなく、単に一部の変数を変更するコードを使用する必要があります。ターゲットスレッドはこの変数を定期的にチェックし、変数が実行停止を指示している場合は、run メソッドから適切に復帰します。条件変数などでターゲットスレッドが長い間待機している場合、待機を中断させるには、interrupt メソッドを使用します。
 void stop(Throwable obj)
推奨されません。 このメソッドは本質的に安全ではありません。stop() (引数なし) を参照してください。さらに、このメソッドは、ターゲットスレッドで処理の準備ができていない例外の生成に使用されることがある点でも危険です (このメソッドがなければスレッドによりスローされることのない、チェックされた例外など)。
 void suspend()
推奨されません。 このメソッドはデッドロックを発生しやすいため推奨されません。ターゲットスレッドが、重要なシステムリソースを保護しているモニターのロックを保持している状態で中断された場合、ターゲットスレッドが再開されるまで、どのスレッドもこのリソースにアクセスできません。ターゲットスレッドを再開しようとするスレッドが、resume を呼び出す前にこのモニターをロックしようとすると、デッドロックが発生します。通常、そのようなデッドロックは、自身を「フリーズした」プロセスとして表明します。
 String toString()
スレッドの名前、優先順位、スレッドグループを含むこのスレッドの文字列表現を返します。
static void yield()
現在実行中のスレッドオブジェクトを一時的に休止させ、ほかのスレッドが実行できるようにします。


12章：マルチスレッド

joinメソッド

    joinメソッドは、前述の通りそのスレッドが終わるのを待機するメソッドです。下の例では、1秒待機するRunnable1と 2秒待機するRunnable2がほぼ同時にスレッドスタートしています。メインメソッドでは２つのスレッドが終了するのを joinで待っており、2つとも終了したらメッセージを出力して終了します。

    もし、joinがなければ、「２つのスレッドが終了しました」というメッセージは、スレッドが終わる前に 出力されてしまいます。

Test.java

class Runnable1 implements Runnable{
    // このrunメソッドがスレッド本体
    public void run(){
        System.out.println("Runnable1 Start!");
        try{
            Thread.sleep(1000);
        }catch(Exception e){}
        System.out.println("Runnable2 End!");
    }
}
class Runnable2 implements Runnable{
    // このrunメソッドがスレッド本体
    public void run(){
        System.out.println("Runnable2 Start!");
        try{
            Thread.sleep(2000);
        }catch(Exception e){}
        System.out.println("Runnable2 End!");
    }
}

public class Test{
    public static void main(String args[]){
        Thread t1 = new Thread(new Runnable1());
        Thread t2 = new Thread(new Runnable2());

        // スレッドを開始
        t1.start();
        t2.start();

        try{
            t1.join();		// t1が終わるまで待機する
            t2.join();		// t2が終わるまで待機する
        }catch(Exception e){}
        System.out.println("２つのスレッドが終了しました");
    }
}

C:\java>javac Test.java

C:\java>java Test
Runnable1 Start!
Runnable2 Start!
Runnable1 End!
Runnable2 End!
２つのスレッドが終了しました

C:\java>


12章：マルチスレッド

5. スレッドの作成(Runnableインターフェース)

    スレッドを作るのにThreadクラスをいちいち継承していたのでは、オブジェクト指向としては非常に不便です。 そこで、Threadクラスを継承しなくても、スレッドを作る方法があります。それはRunnableインターフェースを インプリメントする方法です。

Test.java

// 独自のクラス
class MyRunnable implements Runnable{
    // このrunメソッドがスレッド本体
    public void run(){
        System.out.println("MyRunnable Start!");

        // 10回500ミリ秒毎に"Hello"と表示する
        for(int i = 0 ; i < 10 ; i++ ){
            System.out.println("Hello");
            try{
                // sleepメソッドはThreadクラスの静的メソッドで、
                // nミリ秒(この場合500ミリ秒)待つ(待機状態になる)メソッド。
                // 例外を投げる可能性があるので、キャッチしないといけない
                Thread.sleep(500);
            }catch(Exception e){}
        }

        System.out.println("MyRunnable End!");
    }
}

public class Test{
    public static void main(String args[]){
        // 独自のスレッドクラスのインスタンスを作成
        Runnable r = new MyRunnable();
        Thread t = new Thread(r);

        // スレッドを開始
        t.start();

        // 1秒毎に"Hi!"と10回表示する
        for(int i = 0 ; i < 10 ; i++ ){
            System.out.println("Hi!");
            try{
                Thread.sleep(1000);
            }catch(Exception e){}
        }
    }
}

C:\java>javac Test.java

C:\java>java Test
Hi!
MyRunnable Start
Hello
Hello
Hi!
Hello
Hello
Hi!
Hello
Hello
Hi!
Hello
Hello
Hi!
Hello
Hello
Hi!
MyRunnable End!
Hi!
Hi!
Hi!
Hi!

C:\java>

※アンダーラインの部分が変更点です。
※実行環境によって結果は若干変わる場合があります。


12章：マルチスレッド

6. 同期

    マルチスレッドプログラミングでは、今まで行ってきたプログラムよりも気をつけてプログラムしないと、 今まで予想もしていなかったようなバグがでることがあります。たとえば、銀行の残高計算のプログラムを考えます。 １つ目のスレッドは利子を計算し、２つ目のスレッドがATMから入出金があった場合の処理をします。１つ目のスレッドが 残高をハードディスクから読みとり、さらに現在の金利を別のファイルから読みとります。残高が10万円で、金利が 0.001%だとします。

    と、そこにATMから5万円引き出されたとします。２つ目のスレッドがハードディスクから残高を読みとり、 5万円を引き、残金5万円を新しい残高としてハードディスクに記録します。２つ目のスレッドはこれで終了します。

    １つ目のスレッドに処理が戻り、処理の続きを行いますので、10万円に0.001%の利子をつけて10万1円を新しい 残高としてハードディスクに記録してしまいます。これで１つ目のスレッドも処理が終了しました。

    本当は5万1円にならなくてはいけないのですが、10万1円の残高になっています。また、それぞれのスレッド自体にも バグはありません。

    それぞれのスレッドにバグはなくてもマルチスレッドとして、複数のスレッドが同時に実行されるとこのように 思わぬバグが出てしまいます。

    問題なのは、１つ目のスレッドがハードディスクから、残高を読み出して利子を計算している最中に、 別のスレッドが残高を書き換えてしまったことがバグの原因です。これを防ぐためには、利子計算中は、 入出金をしてはいけないという用にする必要があります。また、入出金処理中は利子計算もしてはいけません。 これを「共有データの同期をとる」と言います。


12章：マルチスレッド

ロック

    ロックとはいわゆる「錠」のことで、１つ目のスレッドがあるメソッドをロックすると、別のスレッドは そのメソッド内に入れなくなります。そのメソッドを出るときにロックを解除することで、別スレッドがその メソッドに入ることができるようにします。

    また、あるオブジェクトをロックすることもでき、１つ目のスレッドがそのオブジェクトをロックすると、 １つ目のスレッドがロックを解除するまでは、２つ目のスレッドはその部分に入れなくなります。

    １つ目の方法は、メソッドを作成するときに、「synchronized」修飾子をつけます。javaでは これだけでロックをかける（同期をとる）ことができます。

Test.java

// 口座クラス
class Account{
    private int balance = 0;

    synchronized void deposit( int amount ){
        int a = balance;
        a = a + amount;
        try{
            Thread.sleep((int)(Math.random()*10));
        }catch(Exception e){}
        balance = a;
    }

    int getBalance(){
        return balance;
    }
}

// 顧客クラス
class Customer extends Thread{
    Account account;

    // コンストラクタ
    Customer(Account account){
        this.account = account;
    }

    // 10円ずつ貯金することを1000回繰り返す
    public void run(){
        for( int i = 0 ; i < 1000 ; i++ ){
            account.deposit(10);
        }
    }
}

class Test{
    public static void main(String args[]){
        // 口座を作る
        Account account = new Account();

        // 10人の顧客を作る
        Customer customers[] = new Customer[10];

        // 10人の顧客が１つの口座に振り込み処理を開始する
        for( int i = 0 ; i < 10 ; i++ ){
            customers[i] = new Customer(account);
            customers[i].start();
        }

        // 10人のスレッドが終わるのを待つ
        try{
            for( int i = 0 ; i < 10 ; i++ ){
                customers[i].join();
            }
        }catch(Exception e){
            System.err.println("Error:"+e);
        }

        // 残高表示
        System.out.println("残高："+account.getBalance());
    }
}

Test.java

// 口座クラス
class Account{
    private int balance = 0;

    void deposit( int amount ){
        int a = balance;
        a = a + amount;
        try{
            Thread.sleep((int)(Math.random()*10));
        }catch(Exception e){}
        balance = a;
    }

    int getBalance(){
        return balance;
    }
}

// 顧客クラス
class Customer extends Thread{
    Account account;

    // コンストラクタ
    Customer(Account account){
        this.account = account;
    }

    // 10円ずつ貯金することを1000回繰り返す
    public void run(){
        for( int i = 0 ; i < 1000 ; i++ ){
            account.deposit(10);
        }
    }
}

class Test{
    public static void main(String args[]){
        // 口座を作る
        Account account = new Account();

        // 10人の顧客を作る
        Customer customers[] = new Customer[10];

        // 10人の顧客が１つの口座に振り込み処理を開始する
        for( int i = 0 ; i < 10 ; i++ ){
            customers[i] = new Customer(account);
            customers[i].start();
        }

        // 10人のスレッドが終わるのを待つ
        try{
            for( int i = 0 ; i < 10 ; i++ ){
                customers[i].join();
            }
        }catch(Exception e){
            System.err.println("Error:"+e);
        }

        // 残高表示
        System.out.println("残高："+account.getBalance());
    }
}

C:\java>javac Test.java

C:\java>java Test
残高：100000

C:\java>
C:\java>javac Test.java

C:\java>java Test
残高：10000

C:\java>
※実行には数分かかります。
※実行環境によっては「synchronized」がない方は結果が異なる場合があります。

    「synchronized」がない場合は、deposit処理中に他のスレッドもdepositを実行してしまい、うまく課金処理が されないことがわかります。一方「synchronized」修飾子がある場合は、１つのスレッドがdeposit処理をしている 間は、他のスレッドはdeposit処理をしないため、性格に実行されるが実行時間が約10倍かかります。


12章：マルチスレッド

ロック

    次に、メソッドにsynchronized修飾子をつけて同期をとる方法ではなく、オブジェクトにロックをかけて、 同期をとる方法を紹介します。

Test.java

// 口座クラス
class Account{
    private int balance = 0;

    void deposit( int amount ){
        int a = balance;
        a = a + amount;
        try{
            Thread.sleep((int)(Math.random()*10));
        }catch(Exception e){}
        balance = a;
    }

    int getBalance(){
        return balance;
    }
}

// 顧客クラス
class Customer extends Thread{
    Account account;

    // コンストラクタ
    Customer(Account account){
        this.account = account;
    }

    // 10円ずつ貯金することを1000回繰り返す
    public void run(){
        for( int i = 0 ; i < 1000 ; i++ ){
            synchronized( account ){
                account.deposit(10);
            }
        }
    }
}

class Test{
    public static void main(String args[]){
        // 口座を作る
        Account account = new Account();

        // 10人の顧客を作る
        Customer customers[] = new Customer[10];

        // 10人の顧客が１つの口座に振り込み処理を開始する
        for( int i = 0 ; i < 10 ; i++ ){
            customers[i] = new Customer(account);
            customers[i].start();
        }

        // 10人のスレッドが終わるのを待つ
        try{
            for( int i = 0 ; i < 10 ; i++ ){
                customers[i].join();
            }
        }catch(Exception e){
            System.err.println("Error:"+e);
        }

        // 残高表示
        System.out.println("残高："+account.getBalance());
    }
}

C:\java>javac Test.java

C:\java>java Test
残高：100000

C:\java>
※実行には数分かかります。

    この方法は、accountにロックをかけています。１つ目のスレッドがaccountにロックをかけると、別のスレッドは ロックが解除されるまで、synchronizedで囲まれたブロック（synchronizedブロックと呼びます）には、入ることが できなくなります。


12章：マルチスレッド

7. デッドロック

    デッドロックとは、マルチスレッドプログラミングで起こりやすいバグの１つです。

    １つ目のスレッドがXというオブジェクトをロックし、Yというオブジェクトのロックが解除されるのを待ちます。 ２つ目のスレッドはYというオブジェクトをロックしており、Xというオブジェクトの解放を待ちます。 このような場合、両方のスレッドはオブジェクトの解放を永遠に待ち続けてしまうため、プログラムは 進まなくなり、いわゆる暴走の状態に陥ります。

    このように、複数のスレッドがお互いにロックの解放を永久に待ち続けてしまうことをデッドロックといいます。

Test.java

class X{
    int x;
}

class Y{
    int y;
}

class A extends Thread{
    X x;
    Y y;

    A(X x, Y y){
        this.x = x;
        this.y = y;
    }

    void func1(){
        System.out.println("func1:start");
        synchronized(x){
            synchronized(y){
                try{
                    sleep(10);
                }catch(Exception e){}
            }
        }
    }

    void func2(){
        System.out.println("func2:start");
        synchronized(y){
            synchronized(x){
                try{
                    sleep(10);
                }catch(Exception e){}
            }
        }
    }

    public void run(){
        for( int i = 0 ; i < 1000 ; i++ ){
            func1();
            func2();
        }
    }
}

class Test{
    public static void main(String args[]){
        X x = new X();
        Y y = new Y();

        A a[] = new A[10];
        for( int i = 0 ; i < 10 ; i++ ){
            a[i] = new A(x, y);
            a[i].start();
        }

        for( int i = 0 ; i < 10 ; i++ ){
            try{
                a[i].join();
            }catch(Exception e){}
        }
    }
}

C:\java>javac Test.java

C:\java>java Test
func1:start
func1:start
func1:start
func1:start
func1:start
func1:start
func1:start
func1:start
func2:start
func1:start
func1:start
func2:start	←ここでデッドロックが発生
※実行環境によって結果は異なります。

    多くの場合、ロックするオブジェクトを同じ順番でロックすれば、デッドロックは防げます。例えば、 上の例では、func2のsynchronizedでロックする順番をfunc1と同じように「x→y」の順番でロックすれば デッドロックは防げます。言い換えるとオブジェクトに優先順位を与えると言うことになります。

    このようにマルチスレッド特有のバグが発生した場合の多くは、バグを再現させることすら難しく、 デバッグ作業は非常に大変な作業になってしまいます。バグが起きないようにあらかじめきちんと設計 をしてからプログラムをする必要があります。


12章：マルチスレッド

8. スレッド間の通信

    スレッドから一時的にロックを解放し、他のスレッドにsynchronizedメソッドもしくはsynchronizedブロックを 実行するチャンスを与えることができます。

例えば、１つ目のスレッドはメッセージを受信するスレッドで、２つめのスレッドはメッセージを与えるスレッドです。 メッセージ受信のスレッドは、メッセージボックスにメッセージがなければメッセージが格納されるまですることがありません。 このような場合他のスレッド（メッセージ送信スレッド）に制御を移します。

    逆にメッセージ送信スレッドは、メッセージボックスがいっぱいならば、メッセージ受信スレッドがメッセージを 処理し終わるまで待たなければいけません。

    このように、他のスレッドが処理し終わるまで待つ場合は「wait」メソッドを使用し、他のスレッドに制御を 与える場合は「notify」または「notifyAll」メソッドを使用します。notifyAllメソッドはwaitしているすべての メソッドに対して通知をしますが、notifyメソッドは、いずれか１つのスレッドに対してのみ通知します。どの スレッドに通知されるかは、JVMが勝手に決めてよいことになっています。

void wait() throws InterruptedException)
現在のスレッドはnotifyもしくはnotifyAllが呼び出されるまで待機します。

void wait(long msec) throws InterruptedException)
現在のスレッドはnotifyもしくはnotifyAllが呼び出されるまで、最大msecミリ秒待機します。

void wait(long msec, int nsec) throws InterruptedException)
現在のスレッドはnotifyもしくはnotifyAllが呼び出されるまで、最大msecミリ秒＋nsecナノ秒待機します。

    以下に例を示します。

Test.java

import java.util.*;

// メッセージボックス
class MessageBox{
    private String message;

    // メッセージボックスにメッセージを入れる
    synchronized void messageIn(String msg){
        try{
            // メッセージがすでに入っていれば、なくなるまで待つ
            while( message != null ){
                wait();
            }

            // メッセージを入れる
            message = msg;

            // メッセージを入れ終わったら別のスレッドを起こす
            notifyAll();

        // 以下例外処理
        }catch(Exception e){
            System.err.println("messageIn:Error:"+e);
            System.exit(1);
        }
    }

    // メッセージ取りだし
    synchronized String messageOut(){
        try{
            // メッセージがなければ、メッセージが入るまで待つ
               while(message == null){
                wait();
            }

            // メッセージ取りだし
            String s = message;
            message = null;

            // 取り出したら別のスレッドを起こす
            notifyAll();
            return s;

        // 以下例外処理
        }catch(Exception e){}
        System.err.println("messageOut:Error");
        System.exit(1);
        return "";
    }
}

// メッセージを作るスレッド
class MessageSender extends Thread{
    public void run(){
        for( int i = 0 ; i < 100 ; i++ ){
            Test.msgBox.messageIn(""+Math.random());
        }
    }
}

// メッセージを取り出し、表示するスレッド
class MessageReciever extends Thread{
    public void run(){
        for( int i = 0 ; i < 100 ; i++ ){
            System.out.println(Test.msgBox.messageOut());
        }
    }
}

class Test{
    public static MessageBox msgBox = new MessageBox();

    public static void main(String arg[]){
        MessageReciever reciever = new MessageReciever();
        reciever.start();

        MessageSender sender = new MessageSender();
        sender.start();

        try{
            sender.join();
            reciever.join();
        }catch(Exception e){}
    }
}

C:\java>javac Test.java

C:\java>java Test
0.2894081823761079
0.20714542091971344

  中略

0.519766868805261
0.6181891980702379

C:\java>
※実行環境によって結果は異なります。

    多くの場合、ロックするオブジェクトを同じ順番でロックすれば、デッドロックは防げます。例えば、 上の例では、func2のsynchronizedでロックする順番をfunc1と同じように「x→y」の順番でロックすれば デッドロックは防げます。言い換えるとオブジェクトに優先順位を与えると言うことになります。

    このようにマルチスレッド特有のバグが発生した場合の多くは、バグを再現させることすら難しく、 デバッグ作業は非常に大変な作業になってしまいます。バグが起きないようにあらかじめきちんと設計 をしてからプログラムをする必要があります。


13章：ガーベージコレクター

15章：ガーベージコレクター

1. ガーベージコレクターとは

    ガーベージ(garbage)とは、不要なデータのことです。それをコレクトするのですからかなりマニアな人の ことなのですが、そうではなく、不要になったデータを集めてそれを処理してくれる機能のことを言います。 例えばC++言語ではあるクラスのインスタンスを作成したら、プログラムでdeleteしてやらないと、不要な データもいつまでもメモリー上に残ることになります。これをメモリーリークと言います。

    また、削除してしまったインスタンスを参照したりするとエラーになってしまいOSなどによっては そのまま暴走してしまうと言ったバグも発生する可能性があります

    しかしJavaではインスタンスを作成しても削除する必要はありません。不必要になったインスタンスは このガーベージコレクターがすべて処理してくれるからです。

class GarbageTest{
    public static void main(String args[]){
        String str1 = new String("abc");
        String str2 = new String("ABC");

        str2 = str1;    // "ABC"という文字列はどこからも参照されなくなり、ガーベージコレクトの対象となる
        System.out.println(str1);
        System.out.println(str2);
    }
}

C:\java>javac *.java

C:\java>java GarbageTest
abc
abc

C:\Mjava>


13章：ガーベージコレクター

2. ガーベージコレクターの実行

    Java Virtual Machine(JVM)はインスタンスの生成が行われた時など、メモリーを厳密に監視しています。 これによって後で、どのメモリー（インスタンス）が参照されていて必要なのか、あるいはもう参照されなくなって 不必要になったのかを調べることができるようにしています。

    しかし不必要になったデータを探すのは、アプリケーションが大きくなればなるほど、時間がかかる処理に なります。したがってJVMはデータが不必要になったらすぐにガーベージコレクトを行うのではなく、暇な時に ガーベージコレクトを行うようにしています。

    実際にガーベージコレクトが行われるタイミングはJVMの実装に任されており、プログラマやユーザーが そのタイミングを関知しなくても良いことになっています。このような理由によってC++にはデストラクタが ありますがJavaにはありません。（デストラクタがいつ実行されるかはわからないからです。）

    Javaのメリットはプラットフォーム(OS)を選ばないことです。パソコンのようなメモリーの比較的豊富な プラットフォームで実行している分にはメモリーのことをそれほど意識しなくても良いのですが、携帯電話など 比較的リソースの乏しいプラットフォームでJavaを実行する場合は、メモリーをできるだけケチって使いたい 場合があります。このような場合、ガーベージコレクトをJVMに任せていてはメモリーが足りなくなってしまいます。

    もちろんメモリーを使用したい時に残りのメモリーが足りなくなればJVMはガーベージコレクターを起動させますが、 もし、時間的に余裕がない時に実行されてしまっては困ります。そこで、プログラマがガーベージコレクタを起動させる タイミングを指定することができます。

    それには、Systemクラスのgc()メソッドを使用します。

    このメソッドはガーベージコレクタを起動させて欲しいとJVMに伝えるだけで、いつ実行させるかはJVM次第です。 よくJavaの解説書のこのメソッドの説明に「ガーベージコレクタを実行する」とありますが、これは間違えです。 あくまでもできるだけ早くガーベージコレクタを実行させて欲しいとJVMに要求するだけです。


13章：ガーベージコレクター

3. finalize

    C++にはデストラクタがあります。デストラクタとはコンストラクタの逆で インスタンスが破壊される時に呼ばれる関数です。しかしJavaにはデストラクタがありません。これは Javaのインスタンスの破棄はガーベージコレクタに任されていて、インスタンスがいつ破壊されるか わからないためです。

    しかし、これでは不便なことがあります。例えばファイルを開いた場合はファイルをクローズするとか、 通信ポートを開いたらそれを閉じるといったメモリー以外のリソースを解放したい場合です。

    このような場合は、Objectクラスのfinalize()メソッドをオーバーライドします。finalize()メソッドは ガーベージコレクタによって、メモリーが解放される前に呼び出されるメソッドです。このメソッドを オーバーライドし、その中でリソースの解放を行います。

    このfinalize()メソッドもいつ呼ばれるかは保証されていません。インスタンスが参照されなくなったことが 確認され、実際にそのインスタンスが占有していたメモリーを解放される間のどこかで実行されることが保証 されているだけです。

    また、ガーベージコレクタが不要になったインスタンスを発見し、まだfinalize()メソッドを実行していない 場合、SystemクラスのrunFinalize()メソッドを実行することで、finalize()の実行を 促すことができます。しかしrunFinalize()メソッドを実行したらすぐにfinalize()メソッドを実行するのではなく 実行するタイミングはJVMの判断に任されています。


14章：有用なクラス

    この章では【結構頻繁に使うクラス】を紹介します。中身まではもちろん覚える必要はなく、 必要な場合はこのページあるいは、 Java APIを参照して頂ければよいと思います。

1. Math

    Mathクラスとはその名の通り数学的なメソッドを集めたクラスです。例えば指数関数、対数関数、平方根、 および三角関数といった基本的な数値処理を行うメソッドなどが用意されています。また、定数として 円周率と自然対数の底が用意されています。

import java.lang.Math

メンバー変数
static double E
        自然対数の底 e にもっとも近い double 値です。
static double PI
        円周とその直径の比 pi にもっとも近い double 値です。

メソッド
static double abs(double a)
        double 値の絶対値を返します。
static float abs(float a)
        float 値の絶対値を返します。
static int abs(int a)
        int 値の絶対値を返します。
static long abs(long a)
        long 値の絶対値を返します。
static double acos(double a)
        指定された角度の逆余弦 (アークコサイン) を、0.0 ～ pi の範囲で返します。
static double asin(double a)
        指定された角度の逆正弦 (アークサイン) を、-pi/2 ～ pi/2 の範囲で返します。
static double atan(double a)
        指定された角度の逆正接 (アークタンジェント) を、-pi/2 ～ pi/2 の範囲で返します。
static double atan2(double y, double x)
        直交座標 (x, y) を極座標 (r, theta) に変換します。
static double ceil(double a)
        引数の値以上で、計算上の整数と等しい、最小の (負の無限大にもっとも近い) double 値を返します。
static double cos(double a)
        指定された角度の余弦 (コサイン) を返します。
static double exp(double a)
        オイラー数 e を double 値で累乗した値を返します。
static double floor(double a)
        引数の値以下で、計算上の整数と等しい、最大の (無限大にもっとも近い) double 値を返します。
static double log(double a)
        指定されたdouble値の自然対数値(底は e)を返します。
static double max(double a, double b)
        2つのdouble値のうち大きい方を返します。
static float max(float a, float b)
        2つのfloat値のうち大きい方を返します。
static int max(int a, int b)
        2つのint値のうち大きい方を返します。
static long max(long a, long b)
        2つのlong値のうち大きい方を返します。
static double min(double a, double b)
        2つのdouble値のうち小さい方を返します。
static float min(float a, float b)
        2 つの float 値のうち小さい方を返します。
static int min(int a, int b)
        2つのint値のうち小さい方を返します。
static long min(long a, long b)
        2つのlong値のうち小さい方を返します。
static double pow(double a, double b)
        1番目の引数を、2番目の引数で累乗した値を返します。
static double random()
        0.0以上で、1.0より小さい正の符号の付いたdouble値を返します。
static double rint(double a)
        引数の値にもっとも近く、計算上の整数に等しいdouble値を返します。
static long round(double a)
        引数にもっとも近いlongを返します。
static int round(float a)
        引数にもっとも近いintを返します。
static double sin(double a)
        指定された角度の正弦(サイン)を返します。
static double sqrt(double a)
        double値の正しく丸めた正の平方根を返します。
static double tan(double a)
        指定された角度の正接(タンジェント)を返します。
static double toDegrees(double angrad)
        ラジアンで計測した角度を、相当する度に変換します。
static double toRadians(double angdeg)
        度で計測した角度を、相当するラジアンに変換します。


14章：有用なクラス

2. Random

    Mathクラスには擬似乱数を発生させるrandomメソッドがありましたが、 それをもう少し高機能にしたものが、このRandomクラスです。

import java.util.Random

コンストラクタ
Random()
        新しい乱数ジェネレータを作成します。
Random(long seed)
        long 型のシードを使って乱数ジェネレータを作成します。

メソッド
protected int next(int bits)
        次の擬似乱数を生成します。
boolean nextBoolean()
        乱数ジェネレータのシーケンスを使って、一様分布のboolean型の次の擬似乱数を返します。
void nextBytes(byte[] bytes)
        ランダムバイトを生成し、ユーザ指定のバイト配列に配置します。
double nextDouble()
        乱数ジェネレータのシーケンスを使って、0.0～1.0の範囲で一様分布のdouble型の擬似乱数を返します。
float nextFloat()
        乱数ジェネレータのシーケンスを使って、0.0～1.0の範囲で一様分布のfloat型の擬似乱数を返します。
double nextGaussian()
        乱数ジェネレータのシーケンスを使って、平均0.0、標準偏差1.0のガウス(「正規」)分布のdouble型の擬似乱数を返します。
int nextInt()
        乱数ジェネレータのシーケンスを使って、一様分布のint型の擬似乱数を返します。
int nextInt(int n)
        乱数ジェネレータのシーケンスを使って、0から指定された値の範囲(0は含むが、その指定された値は含まない) で一様分布の int 型の擬似乱数を返します。
long nextLong()
        乱数ジェネレータのシーケンスを使って、一様分布のlong型の次の擬似乱数を返します。
void setSeed(long seed)
        単一のlong型のシードを使って、乱数ジェネレータのシードを設定します。


14章：有用なクラス

3. System

    Systemクラスは文字通りシステムに関するメソッドとメンバーを併せたクラスです。例えば、コンソール(DOS窓)に 文字を表示させたりするためのPrintStreamインスタンスや、アプリケーション終了に関するメソッド、このプログラムが 実行されてからどのくらい時間が経過したか、ガーベージコレクターに関するメソッドなどがあります。

    Systemクラスはインスタンスを作ることはできません。

import java.lang.System

メンバ
static PrintStream err
        「標準」エラー出力ストリームです。
static InputStream in
        「標準」入力ストリームです。
static PrintStream out
        「標準」出力ストリームです。


C++入門

トップ->Java Applet入門

目次
0. はじめに

1. Javaの基礎
      1. コンパイル方法・実行方法
      2. 予約語
      3. 構成
      4. データの型と宣言
      5. 定数と変数
      6. 簡単な演算
      7. ディスプレイへの表示
      8. 文字列

2. 制御文
      1. 順次構造
      2. 単一分岐
      3. 多重分岐
      4. ケース構造
      5. 反復構造(while)
      6. 反復構造(do)
      7. 反復構造(for)
      8. 補助制御文
      9. 練習問題
      10.練習問題

3. 配列
      1. 配列とは
      2. 配列の宣言と初期化
      3. 配列の使用法
      4. 練習問題
      5. ２次配列と多次元配列
      6. 練習問題

4. クラスの基礎
      1. クラスとは
      2. クラスの作成
      3. コンストラクタ
      4. 例題

5. 演算子
      1. 演算子の種類
      2. インクリメント演算子とデクリメント演算子
      3. 複合代入演算子
      4. ビット演算子
      5. シフト演算子
      6. キャスト演算子
      7. 順次演算子
      8. 条件演算子
      9. instanceof演算子
      10.演算子の優先順位
      11.式と値
      12.条件式と値
      13.練習問題

6. オブジェクト指向とは
      1. オブジェクト指向とクラス
      2. 継承
      3. カプセル化
      4. ポリモーフィズム

7. クラス
      1. thisキーワード
      2. public, protected, private
      3. 静的変数・静的メソッド・静的ブロック
      4. ローカル変数
      5. メソッドの引数
      6. null
      7. 練習問題
      8. インナークラス(内部クラス)
      9. 無名インナークラス(無名内部クラス)
      10. クラスの包含
      11. 練習問題

8. 継承
      1. 継承とは
      2. スーパークラスのコンストラクタ
      3. 継承とキャスト
      4. スコープ
      5. クラスの作成
      6. abstract
      7. 例題）お絵かきソフト
      8. 継承と包含

9. インターフェース
      1. インターフェースとは
      2. インターフェースの宣言
      3. インターフェースの参照
      4. インターフェースの修飾子
      5. インターフェースの継承
      6. インターフェースとinstaceof演算子

10. パッケージ
      1. パッケージとは
      2. パッケージの宣言
      3. クラスパス
      4. import

11. 例外
      1. 例外とは
      2. 例外が起きたら
      3. 例外の種類
      4. catchブロックの検索
      5. throw
      6. throws
      7. エラーと例外
      8. 独自の例外

12. マルチスレッド
      1. スレッドとは
      2. スレッドのライフサイクル
      3. スレッドの作成(Threadクラス)
      4. Threadクラスのメソッド
      5. スレッドの作成(Runnableインターフェース)
      6. 同期
      7. デッドロック
      8. スレッド間の通信

13. ガーベージコレクター
      1. ガーベージコレクターとは
      2. ガーベージコレクターの実行
      3. finalize

14. 有用な(?)クラス群
      1. Math
      2. Random
      3. System
      4. Vector
      5. Stack
      6. Hashtable
      7. String
      8. StringTokenizer
      9. Date
      10. Calendar

15. 入出力
      1.
      2.

16. ネットワーク
      1.
      2.

17. AWT
      1.
      2.

18. Java Applet
      1.
      2.

・ トップページに戻る


Java入門

    Java入門へようこそ。

    JavaはSUNのホームページなどから、無料でコンパイラをダウンロードすることが可能で、 しかもWindowsはもちろんMacやpalmなどPDA、さらには携帯電話をはじめ、さまざまなプラットフォーム で実行できることから注目されているプログラム言語です。

    Javaは大きく分けて３つに分けることができます。１つは普通のワープロソフト同様に、PC上の アプリケーションとして動作するもの、携帯電話やPDAなどで動作するマイクロエディション(Java ME)、 インターネットエクスプローラーやNetscape Navigatorなどのブラウザ上で動作するJava Appletが あります。(Java Scriptはまったく別物です)

    Javaは奥が深いので本ページではJavaの基礎のみ解説をします。

    Javaはどんどんバージョンアップがなされており、JDK(Java Development Kit) Version 1.30が公開 されていますが、本サイトの領域のみでしたら、JDK Version 1.1.xで十分です。「自分のホームページに 自分で作ったJava Appletと掲載したいんだ!!」という人には、特にJDK 1.1.xがお薦めです。 また、他社からもGUIを駆使した統合環境での開発が可能なソフトの旧バージョンが無料でダウンロードできますので、 そちらを使うと便利です。

  • JDK 1.1.8ダウンロードページ
  • JDK 1.4.1およびドキュメントのダウンロードページ
  • JBuilder Version 7ダウンロードページ
出版社 | タイトル | ISBN | 内容
翔泳社 | 独習Java | 4-88135-748-4 | 初心者むけ
サンマイクロシステムズ WebSite
APIがすべて載っている
ダウンロードはこちら
本の検索・ご購入は「 Java関連書」で。

←目次から選んでください

・ トップページに戻る


Index of /~ecb/pic/

v Name                          Last Modified            Size
_____________________________________________________________

Parent directory
akisakura.jpg                    3 Oct 02 00:23        12.7Kb
                      10 Feb 02 01:39        18.3Kb
banner_sm.jpg                   10 Feb 02 01:40        13.6Kb
bgnew.gif                        1 Apr 02 00:18         3.4Kb
catashiato.gif                  10 Feb 02 01:39         0.1Kb
contents.gif                    10 Feb 02 01:39         1.0Kb
freeware.gif                    10 Feb 02 01:39         2.9Kb
grad_b.gif                      10 Feb 02 01:39         1.1Kb
grad_g.gif                      10 Feb 02 01:39         1.5Kb
grayback.gif                    10 Feb 02 01:39        10.0Kb
key.jpg                         10 Feb 02 01:40        26.5Kb
left.gif                        10 Feb 02 01:39         0.9Kb
new.gif                         10 Feb 02 01:39         0.2Kb
nikoniko.gif                    10 Feb 02 01:39         1.9Kb
peach.jpg                       10 Feb 02 01:39        30.0Kb
pen3.gif                        10 Feb 02 01:39         6.6Kb
profile.gif                     10 Feb 02 01:39         1.8Kb
ramen.gif                       10 Feb 02 01:39         3.8Kb
right.gif                       10 Feb 02 01:39         0.2Kb
top.gif                         10 Feb 02 01:39         5.4Kb
vanco.gif                       10 Feb 02 01:39         4.6Kb
_____________________________________________________________