ここでは、クラスの概要について説明していきますが、まずは、構造体に関しての例をあげて、そこからクラスへ拡張していきます。
2つの課題(テスト)に関しての成績処理を行うプログラムを考えます。
扱う要素としては、「氏名」、「課題1の点数」、「課題2の点数」、「平均点」としましょう。これを構造体で表すと以下のようになります。
struct student { char name[20]; //氏名 int kadai1; //課題1の点数 int kadai2; //課題2の点数 int ave; //平均点 };学生を a, b, c の3人とすると、C++では
student a, b, c;
のようにstudent型の構造体(オブジェクト)を宣言します。
C言語では、struct student a, b, c; のように宣言する必要がありますがC++では、1度structで型を定義すると、それ以降はstructを省略することができます。
C言語からの拡張
構造体、共用体、列挙の名前は型名となります。すでに宣言した型を表すには、struct, union, enumなどを特に指定する必要はない。
さて、次に平均点を求める関数を以下のように作ることにします。
void calc_ave(student *x) { x->ave = (x->kadai1 + x->kadai2)/2; }平均点を求める場合はこの関数を使って
calc_ave(&a);
と記述することにより、aのメンバaveに平均点がセットされます。
ここで、構造体(オブジェクト)宣言内で宣言される変数をメンバ、またはメンバ変数とよびます。(この呼び名はクラスの説明でもう一度します)ここで、よりバグの少ないプログラムを作成するには、aveの値はcalc_ave関数内のみで変更することでき、他では変更しないようにする必要があります。つまり、
平均点を求める作業は必ずcalc_ave関数内で行う
ことにします。さらに平均点を返す関数を以下のように作ります。
int average(const student *x) { return(x->ave); }これにより、平均点に対する作業はcalc_aveとaverageを用いればすべて実行できます。
情報隠蔽
平均点に対する作業はcalc_aveとaverageを用いればすべて実行できることがわかったと思います。したがって、
student構造体のユーザは、メンバ ave の存在すら知る必要がない
といえます。ここで、メンバ ave を隠すことができれば、このことを実現することができます。
このように、秘密にした部分を隠し、外部からのアクセスを保護するという概念は、情報隠蔽と呼ばれています。
Cでは、情報隠蔽は、関数やソースファイル単位でしか行うことができませんが、C++では、構造体やクラス単位で行うことができます。具体的には以下のように、ユーザに公表して見せる部分 public と隠す部分 private を用いて定義します。
struct student { public: char name[20]; //氏名 int kadai1; //課題1の点数 int kadai2; //課題2の点数 private: int ave; //平均点 };publicの部分のメンバ(name[20], kadai1, kadai2)を公開部、privateの部分のメンバ ave を私的部とよびます。ユーザに公開する部分は公開部で宣言し、ユーザから隠す部分は私的部で宣言します。それぞれの部分で宣言されたメンバは、公開メンバ、私的メンバ(非公開メンバ)と呼びます。
C言語からの拡張
C++の構造体は、ユーザに公開する公開部(public)と、ユーザに非公開の私的部(private)に分けることができる。
さらに、protected: で指定する保護部があり、そのメンバを保護メンバと呼びます。保護メンバに関しては、継承のところで詳しく説明します。とりあえずここでは、私的メンバと同じような働きをすると覚えておいてください。(実際は違うのですが)
ユーザのアクセス制限を指定する public, protected, privateはアクセス指定子と呼びます。
メンバ関数
aveを非公開のメンバ(変数)に指定したことにより、
a.ave = 100;
のように、非公開メンバへの代入はできませんし、printf("%d¥n",a.ave); もだめです。つまり、aveのメンバにはアクセスできません。平均値aveに対する処理は、calc_aveとaverageの関数で行うようにすると説明しましたが、これまでと同じような関数の定義では情報隠蔽の意味がありません。
そこで、これらの関数は、studentの非公開部にも自由にアクセスできる、特別な関数であることを、構造体studentの中で宣言する必要があります。
このことは、以下のように実現されます。
struct student { public: char name[20]; //氏名 int kadai1; //課題1の点数 int kadai2; //課題2の点数 private: int ave; //平均点 public: void calc_ave(void); //平均点を求める特別な関数 int average(void); //平均点を返す特別な関数 };このように関数も構造体の宣言部で宣言することにより、非公開メンバにもアクセスすることができます。
C言語からの拡張
変数だけではなく、関数も構造体のメンバとなることができる。
構造体の宣言の中で宣言された特別な関数をメンバ関数と呼びます。メンバ関数に対してaveなどの変数をメンバ変数(データメンバ)と呼ぶ。メンバ関数とメンバ変数を併せて、メンバと呼ぶ。
もちろん、メンバ関数を宣言するだけではだめで、それらの関数の中身を定義する必要があります。以下のようにこれまでのような定義とは異なる定義が必要となります。
void student::calc_ave(void) { ave = (kadai1 + kadai2)/2; } int student::average(void) { return(ave); }ここでは、C言語との違いの最後の部分で述べたスコープ解決演算子 :: が使われていることが分かります。student::calc_ave は関数calc_aveが構造体studentのメンバ関数であることを表しています。(正確には、calc_ave関数がstudentクラスのスコープ中にあることを示しています)
以上のようにスコープ解決演算子を用いて構造体(クラス)のメンバ関数を定義します。
ここで、メンバ関数には、操作を行う対象となる構造体を表す引数がないことに気づくでしょう。これまでの例では、
int average(const student *x) { return(x->ave); }のようにstudent *xを引数としています。
メンバ関数では引数を渡す必要がなく、構造体のメンバ変数は、a.kadai1
のようにドット演算子 . で行うのと同様に、メンバ関数の呼び出しも
a.average()
のようにドット演算子を使うため、引数を渡す必要がなくなります。具体的には
student a;
a.kadai1 = 60;
a.kadai2 = 80;
a.calc_ave();
printf("%d¥n", a.average());のように使います。
C言語では、calc_ave(&a); のように構造体 a (aへのポインタ)を関数へ渡しています。一方、C++では、a.calc_ave(); のように構造体(オブジェクト) a に対して関数を呼び出す形となっています。
では、いよいよC++のメイン部分であるクラスについて説明します…、といいたいところですが、これまで、構造体 struct について説明してきましたが、C++でのクラス class は基本的にはstruct と同じです。C++ではどちらもクラスという概念で扱います。
構造体の概念を拡張したものをクラスと呼ぶ
よって、これまで構造体として説明してきた部分はクラスに置き換えることができます。ということで、struct として宣言した部分を class と置き換えることでクラスを宣言できます。struct student { … を class student { …のように。
では、structとclassの違いは何かというと、図2.1に示すように、特にprivate, publicなどのアクセス指定子の指定がなかった場合(デフォルトの場合)、structのメンバはpublicとなるのに対しclassのメンバはprivateとなる点です。この点以外は両者に違いはありません。
図2.1 構造体とクラスの違いクラスを構成するメンバはprivateの方が好ましい、C言語での構造体との違いを明確にするなどの理由によりC++では、structではなく、class を用います。
よって、従来のC言語の構造体として使う場合はstructを使い、それ以外はすべてclassを使うようにしましょう。
Q. struct, class、構造体、クラスという言葉が使われていますが、言葉の意味と関係がよくわかりません?
A. C++の「クラス」の概念が、C言語の「構造体」の概念を拡張したものですので、「クラス」は「構造体」を含んでいると言えます。一方、「struct」や「class」は「キーワード」であり「概念」を表すものではありません。C++では、struct も class もクラスの概念を表すことになります。
クラスとオブジェクト
これまで述べてきたように、クラスとは、データ(変数)とそれを操作する手続きをまとめた雛形を定義したものです。例では、名前、課題の点数、平均値とそれらを操作する2つの手続きを1つにまとめた、studentクラスを定義しました。一方、オブジェクトとは、クラスで定義された雛形を用いて、student a, b, c; のように実際の値として定義されたデータがオブジェクトとなります。この場合は3つのオブジェクトa, b, cを定義したことになります。「オブジェクト」は「もの」ですから、実際に定義されたデータということになります。クラスとオブジェクトは似たような意味を持ちますが、若干違うようです。クラスを基に、実際の値としての定義されたオブジェクトは、インスタンスとも呼ばれます。
studentクラスは、データaveに関する操作は外部から隠蔽するように設計しました。これにより、外部からは公開された手続きを利用することでしかデータaveを操作できないようにすることで、個々のオブジェクトの独立性が高まります。このように、データとそれを操作する手続きをひとまとまりにして、外部からの干渉や誤用から保護することは、カプセル化と呼ばれオブジェクト指向プログラミングの特徴の1つとなっています。
ここでの例では、メンバ関数(calc_ave)のみが、メンバ変数aveの値を変更できます。よって、変数の不正な書き換えを防ぐことができバグの発生を抑えることができます。
Q. 新しい用語がいっぱいでてきて、わけがわからなくなりそうです。まとめてください。
A. こんな感じでどうでしょうか。クラス: データ(メンバ変数)とそれを操作する手続き(メンバ関数)をまとめた雛形を定義したもの
メンバ変数: クラス内で定義される変数。データメンバとも呼ばれる
メンバ関数: クラス内で定義される手続き、関数。オブジェクト指向の世界ではメソッドと呼ばれることもあるが、C++では、メンバ関数を使う。
公開部、公開メンバ: public部分のメンバを公開部、それぞれのメンバを公開メンバと呼ぶ。
私的部、私的メンバ: private部分のメンバを私的部、それぞれのメンバを私的メンバと呼ぶ。非公開部、非公開メンバとも呼ばれる。
保護部、保護メンバ: protected部分のメンバを保護部、それぞれのメンバを保護メンバと呼ぶ。被保護メンバと呼ばれることもある。
オブジェクト: クラスで定義された雛形を用いて、実際の値として定義されたデータ。インスタンスとも呼ばれる。(C++でのオブジェクトです。本来はもっと抽象的な意味を持っています。)
さて、情報隠蔽によるカプセル化は、オブジェクト指向プログラミングにおいて重要なことであることは説明してきました。例として述べてきたstudentクラスでは、name, kadai1, kadai2は公開メンバとなっているため、情報隠蔽をより確実なものとするためには、これらのメンバ変数を私的メンバにする必要があります。この変更によって、直接これらのメンバ変数への代入や表示ができなくなるため、代入や表示をするための関数が必要となります。ここで重要なのは
メンバ変数は、できるだけ公開しないようにする。
ということです。
List 3-1
以上より、studentクラスは以下のように記述できます。クラス名やメンバ関数名の最初の文字を大文字とすることによって、通常の関数名などと区別することにします。
#include <string.h> class Student { char name[20]; //氏名 int kadai1; //課題1の点数 int kadai2; //課題2の点数 int ave; //平均点 public: void SetName(const char *n); //氏名のセット void SetKadai1(int k1); //課題1の点数をセット void SetKadai2(int k2); //課題2の点数をセット void CalcAve(); //平均点を求める char *Name(); //氏名を返す int GetKadai1(); //課題1の点数を返す int GetKadai2(); //課題2の点数を返す int Average(); //平均点を返す }; void Student::SetKadai1(int k1) { kadai1 = k1; } void Student::CalcAve() { ave = (kadai1 + kadai2)/2; } int Student::Average() { return (ave); }皆さん気づいたとは思いますが、List 3-1は、メンバ関数の本体がSetKadai1、CalcAveとAverageしか定義されていませんね。そこで、以下の練習問題です。
List 3-1 では、メンバ関数を、クラス定義の外部で定義しましたが、内部で定義することも可能です。メンバ関数を、クラス定義の内部で定義した例をList 3-2 に示します。
List 3-2
#include <string.h> class Student { char name[20]; //氏名 int kadai1; //課題1の点数 int kadai2; //課題2の点数 int ave; //平均点 public: void SetName(const char *n) { strcpy(name, n); } void SetKadai1(int k1) { kadai1 = k1; } void SetKadai2(int k2) { kadai2 = k2; } void CalcAve(); //平均点を求める char *Name() { return(name); } int GetKadai1() { return(kadai1); } int GetKadai2() { return(kadai2); } int Average(){ return(ave); } }; void Student::CalcAve() { ave = (kadai1 + kadai2)/2; }ここで、
クラス宣言中で定義したメンバ関数は、インライン関数となる。
インライン関数は2.3.3 で説明したとおりマクロと同じような働きをもつものでしたね。
よって、小さなメンバ関数は、インライン関数としてクラス定義の内部で実現したほうがよい。
ということになります。
さらに、クラスはブラックボックスであることが望ましいので、クラス宣言は、ヘッダファイルに記述したほうがよい。
ということになります。ヘッダファイルで定義したクラスを使用する際には、そのヘッダファイルをインクルードすればよいことになります。
また、Studentクラスを使うユーザは、クラス宣言の部分のみを直接必要としているので、メンバ関数などの中身のソースは必要とはしません。したがって、この部分はヘッダファイルには含めずに別のファイルに分割してオブジェクトファイルなどの形として提供すべきです。
とりあえずは、クラスの宣言部と、メンバ関数などの実現部は分割して作成したほうがよい。
と覚えておきましょう。
Q. 小さなメンバ関数はインライン関数としてクラス定義の内部で宣言すると、関数の中身がヘッダファイルに記述されるので、ブラックボックスであるいうことと矛盾するのでは?
A. するどい質問です。 答えはその通りです。矛盾しています。でも、「効率のためだったら、他人に見せるべきではないものも、見られてもよしとしよう」
といったところでしょうか。
練習問題3.4で試したとおり、もし、課題の点数がセットされていなければ、平均点もおかしな値となっていますのがわかります。このような、「変数の初期化忘れ」は頻繁に起こります。
そこで、クラスのオブジェクトが作成されたときに、自動的に初期化を行うことができれば便利です。C++ではオブジェクト(インスタンス)が作成されたときに、その初期化をする関数を設定することができます。このような関数をコンストラクタと呼びます。コンストラクタは、クラスのオブジェクトを初期化するメンバ関数であり、クラスと同じ名前を持つ。
Studentクラスの例ですと以下のようなコンストラクタを定義することができます。
Student::Student(const char *n, int k1, int k2) { strcpy(name, n); kadai1 = k1; kadai2 = k2; CalcAve(); }これによりStudentクラスのオブジェクトの宣言は
Student mariko("濱村真理子", 60, 80);
のように行います。(もちろん、クラス宣言部のpublicでこの関数名をあらかじめ定義する必要があります)
このようにコンストラクタを定義することにより確実にメンバ変数を初期化することができます。なお、コンストラクタは戻り値をもたず、voidと宣言することすらできません。
コンストラクタを定義したことにより
Student a;
のような宣言は、エラーとなってしまうことが練習問題3.5でわかりましたね。
実際に、点数がわからなくてもオブジェクトを宣言する必要もあることがあります。
したがって、引数を渡さなくても初期化を行えるようにする必要があります。
この問題は、2.3.2で説明したデフォルト引数を用いることで解決できます。クラスの宣言部でStudent( const char *n = "", int k1 = 0, int k2 = 0);
のようにコンストラクタを宣言することにより、
Student a("濱村真理子"), b("山田太郎", 50), c("宇都宮花子",80,100);
のようにオブジェクトa (課題1:0点、課題2:0点)、b(課題1:50点、課題2:0点)、c(課題1:80点、課題2:100点)を定義できます。
Studentクラスの作成により
void Student::CalcAve()
におけるスコープ解決演算子 :: はCalcAveはStudentクラスのスコープ中であるということがわかると思います。C言語でのスコープは、ブロックスコープ、関数スコープ、ファイルスコープがありましたが、C++では、これに加えクラススコープがあります。
C言語からの拡張
クラスは1つのスコープを形成する。
では、以下のクラス定義を見てみましょう。
List 3-3
int x; class TestScope { int x; public: int f1() { return(x); } int f2() { return(::x); } int f3() { int x; return(x); } int f4() { int x; return(::x); } int f5(int x); int f6(int x); }; int TestScope::f5(int x) { return(x); } int TestScope::f6(int x) { return(TestScope::x); }さて、関数f1からf6はどの変数xを返すのでしょうか?
練習問題3.7より、
クラスXのメンバYに明示的にアクセスするには
X::Y
のようにスコープ解決演算子 :: を用いる。ということがわかります。 :: 演算子はメンバ変数だけではなくメンバ関数にも
a.Student::SetKadai1(60);
のように使用できます。
任意の長さの文字列を表現するStringクラスを作ってみましょう。
str.h
#include <stdio.h> class String { char *s; public: String(); String(const char *n); void Print() { printf("%s",s); } };
str.cpp (クラスの実現部)
#include <string.h> #include "str.h" String::String() { s = new char[1]; *s = '¥0'; } String::String(const char *n) { s = new char[strlen(n)+1]; strcpy(s, n); }ここでは、2種類のコンストラクタが用意されています。1つが引数なしで宣言された場合のコンストラクタで、この場合は、文字型データにヌル値0を代入しています。もう一方の引数ありで宣言された場合のコンストラクタは、文字列(ポインタ)n を受け取り、文字列の長さ+1(+1はヌルコード分)の領域を動的に確保し、そこに文字列nをコピーしています。動的に領域を確保するには、malloc()使えばよいのですが、2.5 で説明したように、C++では、malloc()ではなく、new, deleteを使うのでしたね。このクラスは、
#include <stdio.h> #include "str.h" int main() { String a; String b("utsunomiya"); printf("a : "); a.Print(); printf("¥n"); printf("b : "); b.Print(); printf("¥n"); return 0; }のように使用できます。しかしこのままでは、newで確保したメモリを解放していないため、最後にdeleteで領域を解放する必要があります。このためには、例えば、メモリを解放するメンバ関数 String::Fin(void) { delete [] s}; を作成し、mainの最後にa.Fin(); b.Fin() のように記述しなければなりません。これは、面倒なだけではなく、メンバ関数Finを最後に呼び忘れてしまう危険性があります。
そこで、C++には、コンストラクタとは逆にオブジェクトが破棄されるときに自動的に呼び出される関数デストラクタ
が用意されています。デストラクタは ~String() のようにクラス名の前に ~ をつけます。Stringクラスにデストラクタを加えると、クラス宣言部は
class String { char *s; public: String(); String(const char *n); ~String() { delete [] s; } void Print() { printf("%s",s); } };のようになります。デストラクタはオブジェクトが破棄される際に自動的に呼び出されるので
デストラクタは、戻り値および引数を持たない。
ということになります。