情報処理 II − 第11回

講義に入る前に shori2 にカレントディレクトリを移しておいてください。


1.データ型の定義


char や int など、最初から用意されているもの以外に、 自分でデータ型を定義することもできます。 このために typedef というキーワードを使います。

typedef unsigned short word;

上の定義によって、 word という新しいデータ型が使えるようになります。 これは unsigned short と同じです。 この名前の決め方は変数名と同じです。 この定義の後から、 次のような変数宣言ができるようになります。

word w;

配列のように複数の要素を持ったデータ型を定義することもできます。

typedef float vector[3];

上の例の場合、 vector は float 型の3個の要素を持つ配列のデータ型になります。 従ってこの定義の後、次のように変数宣言を行うと、 normal は3つの要素を持った配列変数として宣言されます。

vector normal;

定義した型をもとに、さらに新しい型を作ることもできます。

typedef vector matrix[3];

上によって定義された matrix は 3×3の要素を持つ2次元配列変数を宣言します。


課題33


以下で定義されている配列変数 grid の要素の数を数えてください。

typedef int line[10];
typedef line plane[5];
typedef plane cube[4];

cube grid[2];

答えをメールで tokoi まで送ってください。Subject: は kadai33 としてください。


2.構造体


プログラムを書いていると、 複数のデータをひとつにまとめて取り扱いたいと思う場面が 少なからずあります。 成績のデータを例にとって考えてみましょう。

これが一人分のデータだとすれば、 配列を使って次のように表すことができます。

int tensu[3];

この例では、 配列変数 tensu[0] に国語、 tensu[1] に数学、 tensu[2] に英語の点数を格納すると決めておけば、 tensu という配列変数ひとつで、 一人分のデータを保存することができます。

次に、このデータに学生の氏名も含めようと考えたとします。 国語、数学、英語の点数は数値でしたから、 これらはいずれも int 型のデータとして統一できました。 しかし氏名は文字列で表す必要があります。 これは int 型の配列である tensu に含めることはできません。

そう言う場合には構造体 (struct)というデータ型が使えます。

struct shiken {
  char namae[20];
  int tensu[3];
} data;    // shiken というデータ型の data という変数の宣言

これにより学生の指名を格納する char 型の配列変数 namae と各教科の点数を格納する int 型の tensu という配列変数をひとまとめにした、shiken という新しいデータ型が定義され、 そのデータ型の data という構造体変数が宣言されます。。 なお、構造体変数の宣言 (data) を省略すると、構造体の定義のみを行います。

namae や tensu などの構造体の要素のことをメンバと言います。 構造体の各メンバを利用するには、構造体変数名の後に .(ピリオド、 ドット演算子)に続けてメンバ名を書きます。 上の例で宣言された構造体変数 data の各メンバは、 次のように指定します。

// 成績のデータを格納
strcpy(data.namae, "和歌山健太郎");
data.tensu[0] = 88;
data.tensu[1] = 72;
data.tensu[2] = 66;

構造体を一度定義すれば、 同じ構造をもつ構造体変数は 構造体名(上の例では shiken)をデータ型として宣言できます。 同じ構造体同士なら丸ごと代入できます。

shiken anotherData;  // shiken 型の anotherData という変数の宣言
anotherData = data;  // anotherData に data を代入

同じ構造体変数を別のところで宣言する必要がなければ、 構造体名も省略することができます。

struct {
  float x, y;
} point;

構造体変数の初期化の方法は、 配列の初期化に準じます。

shiken wakayama = {
  // 和歌山健太郎の成績データ
  "和歌山健太郎",
  88,
  72,
  66
};

課題34


次のようなデータがあるとします。

shouhintankakosuu
mikan20030
ringo40020
banana30015
suika20003
  1. 下のプログラム中のコメントの部分 (1) を、 このデータを保持する構造体の定義で置き換えてください。
  2. 下のプログラム中のコメントの部分 (2) を、 上のデータを保持するための配列変数の宣言に置き換え、 上の表に従ってそれに初期値を与えてください。
  3. 下は上のデータを集計するプログラムです。 空欄 __(3)__, __(4)__, __(5)__ を埋めて、 プログラムを完成させてください。
#include <iostream.h>

int main(void) {
  /*
   * (1) 構造体の定義をここに書いてください
   */

  /*
   * (2) 配列変数の宣言をここに書いてください
   */

  int i, sum = 0;

  for (i = 0; i < __(3)__; i++) {
    sum += __(4)__ * __(5)__;
  }
  cout << "売上げ合計は" << sum << "円" << endl;

  return 0;
}

作成したソースプログラムをメールに添付して tokoi まで送ってください。Subject:(件名)は kadai34 としてください。


3.クラス


構造体のところで例にあげた shiken という構造体は、 名前に終端文字 '\0' を除いて 19 文字までしか格納できないという制限があります。 また、点数が百点満点であれば、100 点を越える点数や 0 点未満の点数をデータとして取り扱わないようにしなければなりません。

したがって、このデータ型を使ったプログラムを作成するときには、 常にこれら制限について留意しておく必要があります。 仮にプログラム中にこの成績データを操作する部分がいくつも存在するとすれば、 下のようなデータの正当性のチェックを、 プログラム中の至るところに書く必要があります。

//
// 点数の範囲を検査する関数
//
int checkRange(int tensu) {
  if (tensu >= 0 && tensu <= 100) {
    return 1;  // 真〜範囲内にある
  }
  else {
    cout << "点数が範囲を越えています" << endl;
    return 0;  // 偽〜範囲外にある
  }
}

//
// 成績データの設定を行う関数
//
void setdata(char *name, int kokugo, int suugaku, int eigo) {
  if (strlen(name) <= 19) {
    strcpy(data.namae, name);
  }
  else {
    cout << "名前が長すぎます" << endl;
  }
  if (checkRange(kokugo)) {
    data.tensu[0] = kokugo;
  }
  if (checkRange(suugaku)) {
    data.tensu[1] = suugaku;
  }
  if (checkRange(eigo)) {
    data.tensu[2] = eigo;
  }
}

しかしプログラムが大きくなると、 プログラムの各部に散在するこのようなチェックが互いに矛盾しないようにする (プログラムの整合性を保つ) のは結構骨の折れる作業になります。 また、 仮に「成績の上限を 120 点にする」というような変更が必要になったとすれば、 そのすべての部分について同じ変更を加えるのはたいへん面倒な作業になります。

もし、構造体のように自分で構造を持ったデータ型を作成したときに、 そのメンバの利用を特定の関数に制限できれば、 プログラムの局所性を高め、保守性を向上することができます。

このような機能を持ったデータ型をクラス (class)と言います。 クラスには、それに属する関数(メンバ関数) を定義することができます。 クラスの他のメンバ(変数、関数)は、 特に指定しない限り同じクラスのメンバ関数からだけアクセスできます (構造体でも同じようにメンバ関数を定義できますが、 いずれのメンバもメンバ関数以外からアクセスできます)。

seiseki.h
class seiseki {
  char namae[20];           // ここから下はプライベートメンバ
  int tensu[3];             // ↓
public:
  void setNamae(char *);    // ここから下はパブリックメンバ
  void setKokugo(int);      // ↓
  void setSuugaku(int);     // ↓
  void setEigo(int);        // ↓
private:
  int checkRange(int);      // ここからまたプライベートメンバ
};

クラスを用いて定義したデータ型を用いて、 変数を宣言することができます。 この変数をクラス変数と呼びます。

seiseki who;  // seiseki というデータ型の who という変数の宣言

この who のようなクラス変数は、 メンバ関数という形で自分自身の処理方法が定義されている点から、 オブジェクトとも呼ばれます。 またメンバ関数はオブジェクトの処理方法ですから、 メソッドとも呼ばれます。

こうしてメンバ関数を介して値の設定を行うようにして、 値の正当性のチェックをそこに集中させることができます。 もちろんメンバ関数もそのままではメンバ関数以外からアクセスできません。 このようなメンバをプライベートメンバと言います。 メンバをメンバ関数以外からアクセスできるようにするには、 public: というラベルを付けます。 このようなメンバをパブリックメンバと言います。 パブリックメンバの定義からプライベートメンバの定義に戻すには、 private: というラベルを付けます。

上のクラス seiseki では、public: ラベルより後の setNamae(), setKokugo(), setSuugaku(), setEigo() の4つの関数が、 メンバ関数以外からアクセスできます。 このようにクラスの外部とメンバとの仲立ちをするためのメンバ関数を、 特にインタフェース関数と呼びます。

ここで setNamae() は引数に指定した文字列を namae というメンバにコピーするメンバ関数、 setKokugo(), setSuugaku(), setEigo() はそれぞれ国語、数学、英語の点数を tensu[0], tensu[1], tensu[2] に格納するメンバ関数だとします。

これらメンバ関数の宣言には、関数の中身(本体、ボディ)がありません。 これは関数プロトタイプといい、 関数のインタフェース情報(関数名、戻り値や仮引数のデータ型、引数の数など) だけを宣言します。 引数の情報は、ここでは数とデータ型だけを明らかにすれば良いので、 仮引数名はあってもなくても構いません (char *string ではなく char * と書ける)。

このソースプログラムを、例えば seiseki.h というファイル名で保存します。 拡張子が .h のファイルはヘッダファイルといい、 iostream.h や string.h のようにクラスや関数の宣言を記述します。

メンバ関数の本体は別のファイル、例えば seiseki.c というファイル名で、 次のように定義します。 このファイルの先頭には、 cout を使用したときに iostream.h を #include でプログラムの先頭部分に埋め込むのと同様に、 プログラムの先頭部分で seiseki.h を #include を使って埋め込みます。 iostream.h と違って最初から用意されているものではないので、 ファイル名は <...> ではなく二重引用符 "..." ではさみます。

seiseki.cc
#include <iostream.h>
#include <strings.h>
#include "seiseki.h"

// 点数の範囲を検査するメンバ関数
int seiseki::checkRange(int tensu) {
  if (tensu >= 0 && tensu <= 100) {
    return 1;  // 真〜範囲内にある
  }
  else {
    cout << "点数が範囲を越えています" << endl;
    return 0;  // 偽〜範囲外にある
  }
}

// メンバ変数 namae に文字列を格納する
void seiseki::setNamae(char *string) {
  if (strlen(string) <= 19) {
    strcpy(namae, string);
  }
  else {
    cout << "名前が長すぎます" << endl;
  }
}

// メンバ変数 tensu[0] に国語の点数を格納する
void seiseki::setKokugo(int kokugo) {
  if (checkRange(kokugo)) {
    tensu[0] = kokugo;
  }
}

// メンバ変数 tensu[1] に数学の点数を格納する
void seiseki::setSuugaku(int suugaku) {
  if (checkRange(suugaku)) {
    tensu[1] = suugaku;
  }
}

// メンバ変数 tensu[2] に英語の点数を格納する
void seiseki::setEigo(int eigo) {
  if (checkRange(eigo)) {
    tensu[2] = eigo;
  }
}

上の関数名に使われている ::スコープ演算子と呼ばれ、 この関数が seiseki というクラスのメンバ関数であることを示します。 メンバ関数の引数名やメンバ関数内で宣言した変数名などが、 クラスのメンバ名と一致してしまう場合にも、 このスコープ演算子を用いてクラスのメンバであることを明示できます。

// メンバ変数 namae に文字列を格納する
void seiseki::setNamae(char *namae) {
  if (strlen(namae) <= 19) {        // namae は引数
    strcpy(seiseki::namae, namae);  // seiseki::namae はメンバ
  }
  else {
    cout << "名前が長すぎます" << endl;
  }
}

さて、これでいよいよこの seiseki というクラスを使用したプログラムを作ります。 このソースプログラムのファイル名例えば、を main.cc とします。 この最初の部分でも #include で seiseki.h を埋め込みます。

main.cc
#include <iostream.h>
#include "seiseki.h"

int main(void) {
  seiseki who;  // クラス変数(オブジェクト)の定義

  // クラス変数 who にデータを設定
  who.setNamae("和歌山健太郎");
  who.setKokugo(88);
  who.setSuugaku(72);
  who.setEigo(66);

  // クラス変数 who を出力
  who.print();

  return 0;
}

上の main() 関数において、 who の後ろの .(ピリオド、ドット演算子)を使って、 who の個々のメンバ(メンバ関数を含む)を利用しています。

以上の3つのファイルの関係を図に示すと、下のようになります。 seiseki.h には seiseki クラスの定義が記述されており、 main.cc ではそれにしたがってクラス変数の生成が行われます。 一方 seiseki.cc には seiseki クラスのメンバ関数の本体が記述され、 メソッドの実装が具体化されます。 main.cc と seiseki.cc は seiseki.h を介することでクラスの定義を共有しています。

このように、複数のファイルに分割されたファイルをコンパイルするには、 ヘッダファイル以外のソースファイルを CC コマンドの引数に指定します。 ヘッダファイルは #include で拡張子 .cc ソースファイルに埋め込まれるので、 CC コマンドの引数に指定する必要はありません。

% CC main.cc seiseki.cc[Enter]

実際には、これらは必ずしも別のファイルにする必要はありません。 特にメンバ関数の本体をクラス定義の中で定義する (メソッドの本体をクラス定義の中に書く)こともできます。

クラス定義中にメンバ関数を書いた場合
#include <iostream.h>
#include <string.h>

//
// seiseki クラスの定義
//
class seiseki {
  char namae[17];              // 名前
  int tensu[3];                // 点数

// 点数の範囲を検査するメンバ関数
int checkRange(int tensu) {
  if (tensu >= 0 && tensu <= 100) {
    return 1;  // 真〜範囲内にある
  }
  else {
    cout << "点数が範囲を越えています" << endl;
    return 0;  // 偽〜範囲外にある
  }
}

public:

// メンバ変数 namae に文字列を格納する
void setNamae(char *string) {
  if (strlen(string) <= 19) {
    strcpy(namae, string);
  }
  else {
    cout << "名前が長すぎます" << endl;
  }
}

// メンバ変数 tensu[0] に国語の点数を格納する
void setKokugo(int kokugo) {
  if (checkRange(kokugo)) {
    tensu[0] = kokugo;
  }
}

// メンバ変数 tensu[1] に数学の点数を格納する
void setSuugaku(int suugaku) {
  if (checkRange(suugaku)) {
    tensu[1] = suugaku;
  }
}

// メンバ変数 tensu[2] に英語の点数を格納する
void setEigo(int eigo) {
  if (checkRange(eigo)) {
    tensu[2] = eigo;
  }
};

上のようにすると、seiseki.cc において setNamae() などの定義は不要になります。 しかし、 問題を抽象化したオブジェクトの組み合わせでプログラムを作成しようとするなら、 その実装であるメソッドは隠されていた方が望ましいと考えます。 プログラムの骨格部分(この場合 main.cc)を記述している際には、 オブジェクトの実装部分(メソッドの記述、この場合 seiseki.cc)を見ずに、その仕様記述(この場合 seiseki.h)にしたがってプログラム作成(コーディング)を行うようにした方が、 保守性のよいプログラムを作成できます。


課題35


実は上の main.cc で使用しているクラス変数 who(のクラス seiseki)には、 print() というメンバ関数は定義されていません。 そこで、これにクラス seiseki に print() というメンバ関数の定義を追加して、 main.cc と seiseki.cc をコンパイルして作成した実行プログラムを実行したときに、 下のように who というクラス変数のメンバが表示されるようにしてください。

% a.out[Enter]
名前:和歌山健太郎
国語:88点
数学:72点
英語:66点

print() というメンバ関数を追加した seiseki.h と seiseki.cc をメールに添付して tokoi まで送ってください。Subject:(件名)は kadai35 としてください。


4.構造体変数/クラス変数へのポインタ


構造体変数やクラス変数へのポインタは、 通常の変数同様 & 演算子によって得られます。 またポインタ変数も通常の変数と同様 * 演算子を付けて定義あるいは宣言します。

struct shiken {
  char namae[20];
  int tensu[3];
};

//
// 試験データの設定
//
void setShikenData(shiken *p) {
  strcpy((*p).namae, "和歌山健太郎");
  (*p).tensu[0] = 88;
  (*p).tensu[1] = 72;
  (*p).tensu[2] = 66;
}

int main(void) {
  shiken wakayama;

  setShikenData(&wakayama);

  return 0;
}

構造体のポインタ変数の要素の指定は、 .(ピリオド、ドット演算子)の優先順位が * よりも高いため、 上のように括弧を使う必要があります。 しかし、この記法は繁雑なので、 代わりに -> という演算子が用意されています。

//
// 試験データの設定
//
void setShikenData(shiken *p) {
  strcpy(p->namae, "和歌山健太郎");
  p->tensu[0] = 88;
  p->tensu[1] = 72;
  p->tensu[2] = 66;
}

同様にメンバ関数も -> を使って呼び出すことができます。

class seiseki {
  char namae[20];
  int tensu[3];
public:
  void setNamae(char *);
  void setKokugo(int);
  void setSuugaku(int);
  void setEigo(int);
private:
  int checkRange(int);
};

//
// 成績データの設定
//
void setdata(seiseki *p) {
  // クラス変数のポインタの示す場所 p にデータを設定
  p->setNamae("和歌山健太郎");
  p->setKokugo(88);
  p->setSuugaku(72);
  p->setEigo(66);
}

int main(void) {
  seiseki wakayama;

  setdata(&wakayama);

  return 0;
}