C++のクラス概念は,組み込みデータ型と同じように新しい型を作成する道具をプログラマに提供することを目的としている.また,派生クラスとテンプレートは,関連するクラスを構造化し,プログラマがその関係を利用できるようにしてくれる. コンピュータ科学でいう型とは,概念を具体的に表現したものである.例えば,C++の組み込みデータ型とそれが持つ+,-,*などの演算は,数学の四則演算の概念を具体化している.組み込みデータ型に直接対応していない概念を定義するために新しい型を提供するのがクラスである.つまり,クラスはユーザ定義型である.
クラスを理解するために,例として図形を考えよう.図形には色々あるが,例えば三角形を思い浮かべると,三角形には底辺と高さがあり,これによって面積が求まる.この三角形の属性である底辺と高さ,また,面積を求める方法などを1つのまとまりとしたものを三角形クラスという.このとき,底辺や高さをメンバ変数(データメンバ)といい,面積を求めるための方法をメンバ関数(メソッド)という.
この三角形クラスの宣言は,識別子classのあとにクラス名がきて,中括弧{から中括弧}までが本体となる.
クラス宣言(Class Declarations)
クラス宣言を用いて上の事柄を宣言すると次のようになる.
class Triangle
{ private: double base, height; //メンバ変数 public: void assign(double, double); //メンバ関数 double area(); void display(); }; |
ここで,classはクラス宣言を示すキーワードで,次のTriangleはこのクラスの名前である.{と}に囲まれた中は,クラスのメンバ変数とメンバ関数と呼ばれ,メンバどうしの名前が重複しなければ,どのような名前でも使える.また,クラスのメンバはディフォルトでは非公開 (private) メンバになる.したがって,クラスの外から参照する必要のあるコンストラクタやメンバ関数は公開(public)セクションの中におく必要がある. publicの後にはコロン:を必要とする. void display();はTriangleクラスに属するdisplay関数の宣言である.クラス宣言の最後は};で終わる.
このクラスをUML(unified modeling language)を用いて次のように表すことができる.
Triangle |
base
height |
assign()
area() display() |
クラスのオブジェクトを作る
メンバ関数display()は,その持ち主の名前をつけて,t.display()というように用いられる. |
オブジェクトtは普通の変数のように宣言される.この型はTriangleである.これはユーザ定義型(抽象データ型)として考えることができる.
C++では,int , float, doubleなどの型にTriangle型を付け加えることができる.これはオブジェクト t を次のように考えることで理解できる.
また,オブジェクトtを宣言することを,オブジェクトのインスタンス化(instanciation)といい,オブジェクトtをTriangleクラスのインスタンスという.インスタンスとは,そのクラスの特定事例だと考えれば分かりやすい.
クラスを用いたプログラム
解答
戻り値のデータ型 クラス名::メンバ関数名() |
#include <iostream>
using namespace std; class Triangle { private: double base, height; public: void assign(double, double); double area(); void display(); }; //メンバ関数の定義 void Triangle::assign(double b, double h) { base = b; height = h; } double Triangle::area() { return base*height/2; } void Triangle::display() //triangleに属するdisplay関数本体部分の定義 { cout << "三角形(" << base << "," << height << ")" << "の面積=" << area() << endl; } //main関数 int main () { Triangle t; t.assign(18,24.5); t.display(); } |
実行結果
それぞれのメンバ関数の前にはクラス名であるTriangleを付ける.また,::によってメンバ関数はTriangleクラスのメンバであることを意味している.この::をスコープ解決演算子(scope resolution operator)という.
メンバ関数をクラスの中に含めて,
void assign(double, double) {base=b; height=t;}
|
例題10.1のように,メンバ関数がクラス宣言と別に定義されるとき,クラス宣言部分をインターフェイスといい,メンバ関数を含んでいる部分を実装部という.インターフェイス部分がプログラマーがそのクラスを使うときに見る部分で,普段,実装部は別のファイルに隠されている.これにより,情報隠蔽(information hiding)が可能になる.私たちはプログラムするとき,このクラスは何ができるのかを必要とするのであって,どのようにやるのかを知る必要はない.情報隠蔽はカプセル化(encapsulation)と呼ばれ,継承(inheritance),多態(polymorphism)と並んでオブジェクト指向プログラミングの3大重要概念の1つである.
練習問題 10..1 上の例題で,底辺が10,高さが20の三角形オブジェクトobj1を作成し,その面積を求めよ.
|
コンストラクタ(Constructors)
Triangleクラスはassign()メンバ関数を用いて,そのオブジェクトの初期化を行っている.しかし,オブジェクトが宣言されたときに,初期化するほうがより自然である.そこで,C++ ではクラスオブジェクトに対して,初期化が行えるようにコンストラクタメソッドを用意している.
コンストラクタはメンバ関数で,オブジェクトが宣言されると自動的に呼び出される.つまり,先ほどの例題でTriangle tで,Triangleクラスのオブジェクトtが作成されたが,実はコンストラクタが自動的に呼び出されたのである.コンストラクタは,クラスのオブジェクトを作る関数である.そのため,コンストラクタはクラスと同じ名前を取る.
解答
クラス名::コンストラクタ名(型 引数) |
#include <iostream>
using namespace std; class Triangle { private: double base, height; public: Triangle(double b, double h); double area(); void display(); }; //コンストラクタの定義 Triangle::Triangle(double b, double h) { base = b; height = h; } double Triangle::area() { return base*height/2; } //display関数の定義 void Triangle::display() { cout << "三角形(" << base << "," << height << ")" << "の面積=" << area() << endl; } //main関数 int main () { Triangle t(18,24.5); t.display(); } |
実行結果
コンストラクタはassign()と同じ働きをするばかりでなく,Triangle t(18,24.5)により,baseとheightを持つオブジェクトtが作成され,18と24.5がそれぞれbaseとheightに渡される.
Triangle t(18,24.5);は
Triangle t; t.assign(18.24.5); |
public:
Triangle(double b, double h);//コンストラクタ と Triangle::Triangle(double b, double h) { base = b; height = h; } |
Triangle(double b, double h){base = b; height = h;}
また,
double area();
double Triangle::area()
{
return base * height/2;
}
をまとめて,
double area(){return base * height/2;}
と書くことができる.これを自動インライン化という.void display()も自動インライン化を行なうと,次のようになる.
#include <iostream>
using namespace std; class Triangle { private: double base, height; public: Triangle(double b, double h) {base = b; height = h;} //コンストラクタ double area() {return base*height/2;} void display() { cout << "三角形(" << base << "," << height << ")" << "の面積=" << area() << endl;} }; //main関数 int main (void) { Triangle t(18,24.5);//インスタンス化 t.display(); } |
練習問題 10..2 上の例題で,底辺が10,高さが20の三角形オブジェクトobj2をコンストラクタを用いて作成し,その面積を求めよ.
|
インライン(Inline)
inlineを用いると,次のように書くこともできる.C++の開発者Stroustrupはこの書き方を薦めている.
#include <iostream>
using namespace std; class Triangle { public: Triangle(double b, double h) {base = b; height = h;} double area(); void display(); private: double base, height; }; inline double Triangle::area(){return base*height/2;} inline void Triangle::display(){cout << "三角形(" << base << "," << height << ")" << "の面積=" << area() << endl;} //main関数 int main () { Triangle t(18,24.5); t.display(); } |
ここで,Triangleクラスとそのインスタンス化されたオブジェクトの関係を図で表すと次のようになる.
クラスは1つ以上のコンストラクタを持つことができる.
コンストラクタ初期化リスト(Constructor Initializing List)
ほとんどのコンストラクタはオブジェクトのメンバ変数を初期化するほか何もしない.そこで,C++にはこのコードを書くための特殊な記述法が用意してある.それには次のような初期化リスト(initialization list)を用いる.
base(b) |
height(h) |
関数へのオブジェクトの引渡し
関数に引数としてデータを渡すのと同じように,オブジェクトを引数として渡すことができる.それには,渡すオブジェクトの型を使用して関数の仮引数を宣言する.
解答 面積の2乗を求める関数をdouble sq();とし,その引数をTriangle tとする.このとき,面積はt.area()で求まる.
#include <iostream>
using namespace std; class Triangle { private: double base, height; public: Triangle(double b, double h) : base(b), height(h) {} double area(); void display(); }; inline double Triangle::area(){return base*height/2;} inline void Triangle::display(){cout << "三角形(" << base << "," << height << ")" << "の面積=" << area() << end } void sq(Triangle t);\\ //main関数 int main () { Triangle t(18,24.5); t.display(); sq(t); } void sq(Triangle t) { double a = t.area(); cout << "三角形の面積の2乗は" << a*a << endl; } |
実行結果
引数としてオブジェクトのポインタを使う
オブジェクトが沢山のメンバを持つときには,関数の呼び出しが遅くなる.そこで,このような時は,引数としてオブジェクトへのポインタを利用する.つまり,オブジェクトのアドレスを渡すので,メンバごとのコピーの必要がなくなる.
これを達成するには,main()関数の中を,sq(&t);に,ユーザ関数を
void sq(Triangle* t)
{
double a = t area();
cout "三角形の面積の2乗は" a*a endl;
}
とすればよい.
練習問題 10..3 底辺18,高さ24.5の三角形の面積と参照を利用して面積の2乗を求めるプログラムを作成せよ.
|
コピーコンストラクタ(The Copy Constructor)
どのクラスも少なくとも2つのコンストラクタを持っている.これらはその特別な表記によりすぐに見分けることができる.
X( ); // ディフォルトのコンストラクタ
X(const X&); // コピーコンストラクタ |
Triangle( );
|
Triangle(const Triangle&);
|
Triangle x;
|
Triangle y(x);
|
解答
#include <iostream>
#include <cmath> using namespace std; class Point { private: double _x, _y; public: Point(1) : _x(0), _y(0) {} Point(double x, double y) : _x(x), _y(y) {} Point(const Point& P) : _x=P._x,-y=P._y {} Triangle(const Triangle& t) : base(t.base), height(t.height) { cout << "コピーコンストラクタがよばれた" << endl; } double area() {return base*height/2}; void display() {cout << "三角形(" << base << "," << height << ")" << "の面積=" << area() << endl;} }; int main () { Triangle x(18,24.5); Triangle y(x); y.display(); } |
実行結果
練習問題 10..4 中心の座標(x,y)と半径radiusをもつCircleクラスを作成せよ.ただし,このクラスはディフォルトコンストラクタとアクセス関数area()を含むものとする.
|
クラスディストラクタ(Class Destructor)
オブジェクトの生成には,自動的にコンストラクタが関わる.同様に,オブジェクトが消滅するときには,ディストラクタ(destructor)が関わる.
すべてのクラスには1つのディストラクタがある.もし,クラスの中で明示的に定義してなければ,ディフォルトコンストラクタ,コピーコンストラクタ,代入演算子のように自動的に作成される.
クラスディストラクタは,オブジェクトがスコープの最後に到着すると呼ばれる.
クラスディストラクタはコンストラクタの前に〜をつけることにより作成する.
定数オブジェクト(Constant Objects)
オブジェクトが変更されるべきでないときは,定数として宣言しておくのがよいプログラミング技法である.これを行うには,識別子constを用いる.
const char BLANK = ' ';
const int MAX_INT = 2147483647; const double PI = 3.141592653589793; void init(float a[], const int SIZE); |
const Ratio PI(22,7);
|
PI.print();
|
実際,Ratioクラスの定義を変更しないかぎり,定数オブジェクトから呼ぶことができるメンバ関数はコンストラクタとディストラクタだけである.この問題を解決するには,定数オブジェクトから呼ばれるメンバ関数を定数として宣言しておくことである.つまり,
void print() const {cout << num << '/' << den << endl;}
|
const Ratio PI(22,7);
PI.print(); |
動的にオブジェクトを作成する
Xがクラスのとき,X x;でxはクラスX型のオブジェクトになる.また,X* p = new X;により,pはX型へのポインタである.つまり,オブジェクトを動的に作成してポインタにアドレスを代入する.よって,*pはX型のオブジェクトである.ここで,dataがクラスXの公開メンバ変数なら,(*p).dataによりその公開メンバ変数dataにアクセスすることができる.ここで,
2つの表記
(*p).data
p - data
は全く同じ意味を持っている.ポインタを用いているときは,記号-を用いるのが一般的である.さらに,アロー演算子(-)は指し示すものと読めば分かりやすいという特徴も持っている.
解答
#include <iostream>
using namespace std; class X { public: int data; }; int main() { X* p = new X; (*p).data = 22; cout << "(*p).data = " << (*p).data << endl; p-> data = 44; cout << "p -> data = " << p -> data << endl; } |
実行結果
データ構造(Data Structures)
データ構造の1つにリストと呼ばれるものがある.リストには線形リスト(linear list)と連結リスト(linked list)と呼ばれるものがある.線形リストとは,同じ要素が順番に並んでいる構造をいう.線形リストの代表的なものに配列がある.
連結リストは,同じ要素がポインタによって,数珠のようにつながっている構造をいう.連結リストには,単方向リストといい,次のデータのポインタだけを持つもの,双方向リストといい,前と次のデータへのポインタを持つもの,環状リストといい,データが環状に連結されているもの,後入れ先出しリスト(LIFO)といい,後から入れたデータが先に取出されるもの,すでに学んだスタックがこれにあたる.そして,先入れ先出し(FIFO)リストといい,先に入れたデータを先に取り出されるものとがある.キューがこれにあたる.
連結リストの基本は自己参照クラスとポインタである.連結リストの要素は,一般にノード(node)またはセル(cell)とよばれ,自己参照クラスで表現される.
class Node
{
public:
Node(int d, Node* q = 0) : data(d), next(q) { }
int data;
Node* next // 自己参照クラス
};
これにより,Nodeクラスのオブジェクトは,int dataメンバとnextポインタノードを含んでいる.ここで,Nodeクラスの定義はクラス自身への2つの参照を含んでいる.これは,それぞれの参照がクラスへのポインタであることから許されている.また,コンストラクタが両方のメンバ変数を初期化していることにも注意して欲しい.
解答 上の説明で,連結リストを作成するためのNodeクラスを定義した.Nodeクラスはデータを格納するメンバ変数dataと次のNodeを指すメンバ変数nextを持っている.そこで,ファイルの終端文字 Dが入力されるまでwhileループによりメンバ変数nに整数を読み込む.そして,新しいノードを取り込み,メンバ変数pに整数を挿入する.
forループはリストpが指しているノードからはじめ,pがナルになるまで全探索する.キーボードから67が最後に入力されれば,pを指しているノードは67を含んだノードとなる.
この例題で作成されるリストは次のように考えることができる.
#include <iostream>
using namespace std; class Node { public: Node(int d, Node* q=0) : data(d), next(q) { } int data; Node* next; // 自己参照クラス }; int main() { int n; Node* p; Node* q=0; while(cin >> n){ p = new Node(n,q); q = p; } for (; p; p=p-> next){ cout << p-> data << " -> "; } cout << "*\n"; } |
実行結果
静的メンバ変数(Static Data Member)
メンバ変数の1つの値がクラスの全てのメンバ変数に適応されることがある.このような場合,全てのメンバ変数に同じ値を格納しておくのは効率的ではない.そこで,このことを防ぐ方法として,メンバ変数をスタティック(static)で宣言しておく方法がある.その構文は次のようになる.
class X
{
static int n; // nを静的データとして宣言
};
int X::n = 0; // nの定義
静的メンバ変数は自動的に初期化される.したがって,最後の行は零以外の初期値を必要とするとき以外,書いても書かなくてもよい.
#include <iostream>
using namespace std; class Thingamabob { public: Thingamabob() {++count;} ~Thingamabob() {--count;} static int count; }; int Thingamabob::count=0; int main() { Thingamabob w, x; cout << "Now there are " << w.count << " Thingamabobs." << endl; { Thingamabob w,x,y,z; cout << "Now there are " << w.count << " Thingamabobs." << endl; } cout << "Now there are " << w.count << " Thingamabobs." << endl; Thingamabob y; cout <<< "Now there are " << w.count << " Thingamabobs." << endl; } |
最初に2個のThingamabobが作られ,次に内側のブロックで4つのThingamabobが生成され全部で6個になるが,プログラムコントロールがそのブロックを去るとき消滅してまた2個に戻っている.
静的メンバ変数は大域変数のように,インスタンスの数に関わらず1つのコピーしか持たない.大域変数との違いは,関数なので非公開.
実行結果
#include <iostream>
using namespace std; class Thingamabob { public: Thingamabob() {++count;} ~Thingamabob() {--count;} int numThingamabobs() {return count;} private: static int count; }; int Thingamabob::count=0; int main() { Thingamabob w, x; cout << "Now there are " << w.numThingamabobs() << " numThingamabobs." << endl; { Thingamabob w,x,y,z; cout << "Now there are " << w.numThingamabobs() << " numThingamabobs." << endl; } cout << "Now there are " << w.numThingamabobs() << " numThingamabobs." << endl; Thingamabob y; cout << "Now there are " << w.numThingamabobs() << " numThingamabobs." << endl; } |
この例題では,静的メンバ変数countが非公開である.したがって,countにアクセスするにはアクセス関数 numThingamabob()が必要である.
実行結果
静的メンバ関数(Static Member Functions)
メンバ関数を staticで宣言すればよい. |
#include <iostream>
using namespace std; class Thingamabob { public: Thingamabob() {++count;} ~Thingamabob() {--count;} static int num() {return count;} private: static int count; }; int Thingamabob::count=0; int main() { cout << "Now there are " << Thingamabob::num() << " Thingamabobs." << endl; Thingamabob w, x; cout << "Now there are " << Thingamabob::num() << " Thingamabobs." << endl; { Thingamabob w,x,y,z; cout << "Now there are " << Thingamabob::num() << " Thingamabobs." << endl; } cout << "Now there are " << Thingamabob::num() << " Thingamabobs." << endl; Thingamabob y; cout << "Now there are " << Thingamabob::num() << " Thingamabobs." << endl; } |
実行結果
1. クラスの公開メンバと非公開メンバとの違いを説明せよ.
2. クラスメンバ関数とアプリケーション関数との違いを説明せよ.
3. ディフォルトコンストラクタとそれ以外のコンストラクタとの違いを説明せよ.
4. C++におけるclassとstructとの違いを説明せよ.
5. コンストラクタにはどんな名前を付けることができるか.また,ディストラクタにはどんな名前を付けることができるか.
6. 1つのクラスには何個のコンストラクタとディストラクタを含むことができるか.
7. 次のコードでコピーコンストラクタは何回呼ばれたか.
Thingamabob f(Thingamabob u)
{
Thingamabob v(u);
Thingamabob w = v;
return w;
}
int main()
{
Thingamabob x;
Thingamabob y = f(f(x));
}
1. 空間上の点(x,y,z)からなるPointクラスを作成せよ.ただし,このクラスは,ディフォルトコンストラクタ,コピーコンストラクタ,点の座標を負にするnegate()関数,距離を表すnorm()関数,そして表示のためのprint()関数を含むとする.
2. 整数のスタックのStackクラスを作成せよ.ディフォルトコンストラクタ,ディストラクタ,push(), pop(), isEmpty(), isFull()関数を含むものとする.