ユーザ定義型
型とは,コンセプトを具体的に表現したものである.例えば算術演算型とそれが持つ+や-,*などの演算は,数学の実数概念の概要を具体化している.プログラマは「組み込みデータ型に直接の対応物を持たないコンセプト」を定義するために新しい型を定義できる.それらの型はユーザ定義型と呼ばれる.ユーザ定義型は次の4種類ある.
- クラス
- 構造体
- 共用体
- 列挙体
クラス
クラスとはユーザ定義型の1つである.C++のクラスの概念は,プログラマに組み込みデータ型と同じように便利に使える新しい型を作成するツールを提供することを目的としている.
クラスは,関数や変数,定数などの要素(メンバ)を一緒に保持する型である.あるクラスは別のクラスを作るための基にすることができる(継承).この時,基になったクラスを基底クラス,基にしたクラスを継承クラスと呼ぶ.プログラマは,クラスのメンバに対する継承クラスからのアクセスや生成されたオブジェクトからのアクセスを制限すること(アクセス制御)ができる.全てのクラスは,生成される際にコンストラクタと呼ばれる初期化関数のようなものによって,自動的に初期化される.そして,一部のクラスはオブジェクトが解体される際にデストラクタと呼ばれる関数のようなものによって,資源の後始末などを行う.
class-head { member-specificationopt }
- class-head
-
- class-key identifieropt base-clauseopt
- class-key nested-name-specifier identifier base-clauseopt
- class-key nested-name-specifier template template-id base-clauseopt
- class-key
-
- class
- struct
- union
- 補足
-
- identifierは名前(識別子).
- member-specificationoptはメンバに関する記述で,各メンバの定義やそのアクセス制御.
- base-clauseoptは継承をする際に使用.
- nested-name-specifierは完全修飾名.
- class-or-namespace-name :: nested-name-specifieropt
- class-or-namespace-name ::template nested-name-specifier
- class-or-namespace-nameは名前空間名かその別名,クラス名,template-idのいずれか.
- template-idの詳細は「テンプレート」で説明する.
また,Cとの互換性を保つために,クラスと同じ名前の非クラスは,同じスコープで宣言できる.
メンバ
クラス定義では,各メンバの定義やそのアクセス制御を行う.アクセス制御については後で説明する.文法については以下の通りである.
- member-specification
-
- member-declaration member-specificationopt
- access-specifier : member-specificationopt
各メンバの定義のための文法は次のようになる.
- member-declaration
-
- decl-specifier-listopt member-declarator-listopt ;
- function-definitionopt;
- ::opt nested-name-specifier templateopt unqualified-id ;
- using-declaration ;
- template-declaration ;
- member-declarator-list
-
- member-declarator
- member-declarator-list member-declarator
- member-declarator
-
- declarator pure-specifier
- declarator constant-intializeropt
- identifieropt : constant-expression
- 補足
-
- function-definitionは関数の定義.
- template-declarationはテンプレートの定義.
- decl-specifier-listは
- declaratorは.
- identifierは名前(識別子).
- nested-name-specifierは完全修飾名.
- access-specifierは「カプセル化」で説明する
- constant-expressionは定数式.
- constant-initializerは" = constant-expression".
- pure-specifierは" = 0".
- unqualified-idは.
- using-declarationはusing宣言やusingディレクティブ.
- は.
C++のクラスの特性を理解するために,これから日付の概念を構造体といくつかの関数によって実装する例を考える.
struct Date {
int d, m, y; // dは
};
void init_date(Date& d, int dd, int mm, int yy); // dを第2-4引数を使って初期化
void add_year(Date& d, int n); // d.yにn年を加算
void add_math(Date& d, int n); // d.mにn年を加算
void add_day(Date& d, int n); // d.dにn年を加算
struct Date {
int d, m, y; // dは日,mは月,yは年の情報を保持
void init(int dd, int mm, int yy); // dとm,yを第1-3引数を使って初期化
void add_year(int n); // yにn年を加算
void add_math(int n); // mにn年を加算
void add_day(int n); // dにn年を加算
};
void Date::init(int dd, int mm, int yy)
{
d = dd;
m = mm;
y = yy;
return;
}
演算子の多重定義
C++のプログラマは,後述する演算子トークンの演算子(すなわち+や&らは単項演算子と二項演算子の両方)を多重定義できる.この時,基の演算子と新たに定義する演算子の項の数は等しい(異なる場合はエラー).
- operator
-
- +
- -
- *
- /
- %
- ^
- &
- |
- ~
- !
- =
- <
- >
- +=
- -=
- *=
- /=
- %=
- ^=
- &=
- |=
- <<
- >>
- >>=
- <<=
- ==
- !=
- <=
- >=
- &&
- ||
- ++
- --
- ->*
- ,
- ->
- []
- ()
- new
- new[]
- delete
- delete[]
- function-definition
- decl-specifier-listopt declarator ctor-initializeropt function-body
- decl-specifier-listopt declarator try ctor-initializeropt function-try-body handler-list
- 補足
- declarator の詳細は後述するが,演算子の多重定義にはoperator-function-id を指定.
- ctor-initializerはメンバ初期化リストを使用する際に記述.
- function-bodyは複合文.
- declarator
- direct-declarator
- ptr-operator declarator
- direct-declarator
- ::opt operator-function-id
- direct-declarator ( parameter-declaration-clause ) cv-qualifier-listopt exception-specificationopt
- direct-declarator [ constant-expressionopt ]
- (declarator )
- operator-function-id
- operator operator
- 補足
- parameter-declaration-clauseは引数の数や型の定義.
- cv-qualifier-list はconstかvolatileの修飾子による0回以上の修飾のこと.
- exception-specificationは例外スローの制限を指定.
- constant-expressionは定数式.
- 前置の単項演算子
引数を取らない静的でないメンバ関数,あるいは1つの引数を取る非メンバ関数として宣言できる.
- 静的でないメンバ
class T { T operator@(){} };
- 非メンバ関数
T operator@(T t){}
- 二項演算子
1つの引数を取る静的でないメンバ関数,または2つの引数を取る非メンバ関数のどちらとして宣言しても良い.
- 静的でないメンバ
class T { T operator@(T t2){} };
- 非メンバ関数
T operator@(T t1, T t2){}
- 代入演算子と複合代入演算
静的でないメンバ関数でなければならない.そしてそれは継承されず,プログラマがoperator=を定義しないならば,標準ではそのクラスのメンバのメンバ毎の代入として定義される.
- 関数呼び出し
静的でないメンバ関数でなければならない.()演算子は1つ目の項に一次式,2つ目の項に0以上の引数列を持つ二項演算子とみなす.そのメンバ関数の引数は,引数列の引数の数と同じ.
struct T { void operator()(int arg1, int arg2){ std::cout << arg1 << ", " << arg2 << std::endl; return;} }; int main() { T t; t(1, 1000); return 0; }
- 添字付け
静的でないメンバ関数でなければならない.[]演算子は1つ目の項が,2つ目の項が添字の二項演算子として扱われる.
- クラスメンバ・アクセス
単行演算子として考えられる.operator->はは静的でないメンバでなければならない.
- インクリメントとデクリメント
1つの引数を取るoperator++やoperator--は前置のインクリメントやデクリメント,2つの引数を取るoperator++やoperator--は後置のインクリメントやデクリメントに対応する.後置の場合,第二引数は必ずint型でなければならず,演算子として呼び出された時第二引数には0が与えられる.後置の時は,1つの引数を取る静的でないメンバ関数,または2つの引数を取る非メンバ関数のどちらとして宣言しても良い.前置の時は,引数を取らない静的でないメンバ関数,または1つのint型の引数を取る非メンバ関数のどちらとして宣言しても良い.
- 静的でないデフォルトコンストラクタを持たない型のオブジェクトメンバ
- 静的でない定数オブジェクトメンバ
- 静的でないリファレンスオブジェクトメンバ
- function-definition
- decl-specifier-listopt declarator ctor-initializeropt function-body
- decl-specifier-listopt declarator try ctor-initializeropt function-try-body handler-list
- ctor-initializer
- : mem-initializer-list
- mem-initializer-list
- mem-initializer
- mem-initializer , mem-initializer-list
- mem-initializer
- mem-initializer-id (expression-listopt )
- mem-initializer-id
- ::opt nested-name-specifieropt class-name
- identifier
- 補足
- declarator 内のid-expressionにはクラス名が入る(コンストラクタだから).
- フレンド関数
-
フレンド関数はメンバ関数ではない.それゆえ,フレンド関数の定義は,グローバルスコープで行う.
class Hoge { private: int x; friend void foo(); public: Hoge(){ x = 100; } ~Hoge(){} }; void foo() { Hoge h; std::cout << h.x << std::endl; return; } int main() { Hoge h; h.foo(); // エラー foo(); // ok return 0; }
- フレンドクラス
-
class Hoge { friend class Fuga; private: int x; public: Hoge(){ x = 100; } ~Hoge(){} }; class Fuga { public: Fuga(){ Hoge h; std::cout << h.x << std::endl; } ~Fuga(){} }; int main() { Fuga f; std::cout << f.x << std::endl; // エラー return 0; }
- フレンドクラス(非公開派生の場合)
class Hoge { friend class Fuga; void func(){std::cout << "hoge\n";} }; class Fuga : private Hoge { public: Fuga(){ func(); } };
- 公開(public)メンバ
- 非公開(private)メンバ
- 限定公開(protected)メンバ
- access-specifier : member-specificationopt
- access-specifier
- private
- protected
- public
- 基底クラスのprivateメンバの名前の隠蔽ができる.
- オブジェクトに無効な(意図しない)値の代入を防ぐことができる(例↓).
class Year { private: // 省略可能 unsigned int y; public: void change(int yy){ // changeを呼ぶことで値を変更できる if (1 <= yy && yy <= 12) y = yy; return; } int val(){ // valを呼ぶことで値を取得できる return y; } Date(int yy = 1){ change( yy ); } // コンストラクタ:引数がない時yearは1になる };
- 基底クラスのアクセス制御
- access-specifier virtualopt ::opt nested-name-specifieropt class-name
- access-specifieropt virtual ::opt nested-name-specifieropt class-name
- 補足
- nested-name-specifierは完全修飾名.
- class-nameは名前(識別子)かtemplate-id.
- virtualキーワードについての詳細は「仮想基底クラス」で.
- <未完成-_-.>
- base-clause
- : base-specifier-list
- base-specifier-list
- base-specifier
- base-specifier-list , base-specifier
- base-specifier
-
- ::opt nested-name-specifieropt class-name
- access-specifier virtualopt ::opt nested-name-specifieropt class-name
- access-specifieropt virtual ::opt nested-name-specifieropt class-name
- 補足
- nested-name-specifierは完全修飾名.
- class-nameは名前(識別子)かtemplate-id.
- template-idの詳細はテンプレートのところで.
- access-specifierはアクセス制御.
- 名前の隠蔽
-
基底クラスが重複した場合(菱形継承)の例を示す.
このような場合,派生クラスで同じ名前のメンバを宣言することで基底クラスの名前を隠蔽し,エラーを解決できる.
class Hoge { public: int x; Hoge(){} ~Hoge(){} }; class Fuga : public Hoge { public: Fuga(){x=10;} ~Fuga(){} }; class Piyo : public Hoge { public: Piyo(){x=100;} ~Piyo(){} }; class Foo : public Fuga, public Piyo { public: Foo(){x=1000;} // エラー:曖昧であるため ~Foo(){} };
class Hoge { public: int x; Hoge(){} ~Hoge(){} }; class Fuga : public Hoge { public: Fuga(){x=10;} ~Fuga(){} }; class Piyo : public Hoge { public: Piyo(){x=100;} ~Piyo(){} }; class Foo : public Fuga, public Piyo< { public: int x; Foo(){x=1000;} ~Foo(){} };
- 上書き(オーバーライド)
-
複数の基底クラスのメンバ名の重複する例を示す.
複数の基底クラスの仮想関数が重複する場合,単一の関数によって上書きできる.
class Hoge { public: Hoge(){} ~Hoge(){} virtual void func(){std::cout << "hoge\n";return;} }; class Fuga { public: Fuga(){} ~Fuga(){} virtual void func(){std::cout << "fuga\n";return;} }; class Piyo : public Hoge, public Fuga { public: Piyo(){} ~Piyo(){} }; int main(){ Piyo p; p.func(); // エラー:曖昧であるため return 0; }
class Hoge { public: Hoge(){} ~Hoge(){} virtual void func(){std::cout << "hoge\n";return;} }; class Fuga { public: Fuga(){} ~Fuga(){} virtual void func(){std::cout << "fuga\n";return;} }; class Piyo : public Hoge, public Fuga { public: Piyo(){} ~Piyo(){} void func(){std::cout << "piyo\n";return;} }; int main(){ Piyo p; p.func(); return 0; }
- using宣言
-
複数の基底クラスのメンバ名の重複する例をもう1つ示す.
このような場合は,名前の隠蔽やオーバーライドをしなくても解決可能である. 基底クラスの曖昧な関数は,派生クラスでusing宣言を行うことで曖昧性を解消できる.例えば,呼び出される関数をHoge::fooにしたいならばPiyoでusing宣言を行えば良い.
class Hoge { public: Hoge(){} ~Hoge(){} int foo(int i){ return i;} }; class Fuga { public: Fuga(){} ~Fuga(){} float foo(float f){ return f;} }; class Piyo : public Hoge, public Fuga { public Piyo(){} ~Piyo(){} }; int main() { Piyo p; std::cout << p.foo(0.5) << std::endl; // エラー:曖昧であるため return 0; }
class Hoge { public: Hoge(){} ~Hoge(){} int foo(int i){ return i;} }; class Fuga { public: Fuga(){} ~Fuga(){} float foo(float f){ return f;} }; class Piyo : public Hoge, public Fuga { public Piyo(){} ~Piyo(){} using Hoge::foo; }; int main() { Piyo p; std::cout << p.foo(0.5) << std::endl; return 0; }
- 仮想基底クラス
- 菱形継承のエラーの例のように,基底クラスが重複する場合にデータを共有することでエラーを解決できる.詳細は「仮想基底クラス」で説明する.このデータの共有を指定する仕組みを仮想基底クラスという.
- base-specifier
- access-specifier virtualopt ::opt nested-name-specifieropt class-name
- access-specifieropt virtual ::opt nested-name-specifieropt class-name
struct T { int x; };
void operator+(T v){ std::cout << "hoge\n"; return;}
int main()
{
T t;
operator+(t); // ok
+t; // ok
return 0;
}
演算子関数の定義は,演算子に応じて決まった方法がある.静的でないメンバ関数としての定義の場合,演算子関数の引数の数は項の数-1,非メンバ関数の場合,演算子関数の引数の数は項の数に等しい.以下にいくつかの定義の例を示す(Xはクラス,@は演算子トークンを表す).
C++では,新しい演算子トークンは定義できない.しかし処理内容自体は,関数呼び出し記法で代用できる.
コンストラクタ(構築子)
あるオブジェクトの初期化のために関数を用意して使用することは,初期設定を忘れたり2回行ったりというミスを行う要因を含む.そこでC++では,コンストラクタ(構築子)と呼ばれるオブジェクトが生成される際に自動的に呼び出される関数もどきが用意されている.
struct Hoge{
int x;
Hoge();
};
Hoge::Hoge(){
x = 10;
}
コンストラクタの本体にあるreturn文は,戻り値を指定できない. コンストラクタのアドレスは取得できない.
明示的なコンストラクタ
コンストラクタによるユーザ定義変換は,標準では暗黙的に呼び出される.しかし,explicitで修飾することで,それを回避できる.このようなコンストラクタをexplicitコンストラクタと呼ぶ.
class Hoge
{
public:
int x;
explicit Hoge(int i){ x = i;}
}
Hoge h = 0; // エラー
Hoge g = Hoge(0); // ok
デフォルトコンストラクタ
デフォルトコンストラクタとは,引数を指定せずに呼び出せるコンストラクタのことである.いくつかの組み込みデータ型もデフォルトコンストラクタを持つ.引数があるかどうかに関係なく,引数を省略可能なもの(デフォルト引数を持つ)を指す.
int x = int();
組み込みデータ型のデフォルトコンストラクタは,その型に変換した0(論理型のfalseや,文字型のNULL文字など)を返す.
プログラマがデフォルトコンストラクタを宣言している場合には,それが使用される.ただしデフォルトコンストラクタも他のコンストラクタも宣言しておらず,かつデフォルトコンストラクタが必要な場合は,コンパイラがデフォルトコンストラクタを生成しようとする.生成されたデフォルトコンストラクタは,基底クラス群のデフォルトコンストラクタを暗黙的に呼び出す.基底クラスとは,そのクラスを基にしたクラスらのことである.継承についての詳細は後述する.また,何らかの型のポインタのコンストラクタは構文エラーとなる.
初期化
基本的にクラスのメンバは定義順に初期設定される(定義でない宣言の順ではない).つまりソースコードの上に書いたもの下に書いたものよりも先に初期化される.同様に左に書いたものは右に書いたものより早い.
クラス型(および構造体)のオブジェクトは,コンストラクタによって初期化される.この時,明示的に指定しない限りデフォルトコンストラクタが呼び出される.また,組み込みデータ型のオブジェクトは,明示的に初期設定しない限り初期設定されない.この仕様はCに対する互換性を確保するためである.また,記号定数(const)と参照型は初期設定が必要なので,プログラマがそれらに対して明示的に初期設定をしない限り,それらの型のメンバを持つクラスは,デフォルトコンストラクタを生成できない.以下に例を示す.
struct X {
const int a;
const int& r;
};
X x; // エラー
基底クラスがコンストラクタを持つ時,派生クラスのコンストラクタの呼び出し前に呼び出される.
クラスが基底クラスを多重継承している場合,基底クラスのコンストラクタは宣言順に,最後に自身のコンストラクタが呼び出される.基底クラスの宣言順序は左から右の深さ優先探索が行われる.例えば次のようなコードがあったとする.
struct A { A(){std::cout << "A\n";} };
struct B : A { B(){std::cout << "B\n";} };
struct C : A { C(){std::cout << "C\n";} };
struct D { D(){std::cout << "D\n";} };
struct E : B, C, D { E(){std::cout << "E\n";} };
E e;
仮想基底クラスの場合,コンストラクタは1度だけしか呼び出されない.例えば,前述の例の構造体Aの継承を仮想基底クラスに変更したとする.
struct A { A(){std::cout << "A\n"; return;} };
struct B : A { B(){std::cout << "B\n"; return;} };
struct C : A { C(){std::cout << "C\n"; return;} };
struct D { D(){std::cout << "D\n"; return;} };
struct E : B, C, D { E(){std::cout << "E\n"; return;} };
E e;
ちなみに破壊(デストラクタ)はこの逆順に行われる.
配列のコンストラクタの呼び出しは,下位アドレスから上位アドレスに向かう順序で行われる.
メンバ初期化リスト
メンバの初期化方法にメンバ初期化リストやメンバイニシャライザと呼ばれるものを用いる方法がある. 次に示すオブジェクトメンバは,メンバ初期化リストでのみ初期化できる.
class Hoge
{
public:
const int a = 10; // エラー
int y;
const int x;
Hoge(int n) : x(n), y(100) { std::cout << x << std::endl; }
~Hoge(){}
};
int main()
{
Hoge h(1024);
return 0;
}
デストラクタ
公開されていなければならない. デストラクタは,関数が通常通りに終了したか,例外が投げられたので終了したかに関わらず必ず呼び出される.
フレンド
クラスの非公開部は,通常外部からアクセスできない.しかしfriend宣言を用いることで,特定の関数やクラスからのアクセスが可能になる.このような関数をフレンド関数,クラスをフレンドクラスと呼ぶ.フレンドクラスやフレンド関数を持つクラスを基底とする継承クラスでは,それらの効果は反映されない.friendは,密接に結びついた概念を表現する場合にのみに使用する.
クラス内で宣言されたメンバ関数は,修飾子や宣言によっていくつかの性質を変更できる.
修飾子なし | static | friend | |
---|---|---|---|
非公開メンバへのアクセスを制限する. | ○ | ○ | ○ |
クラススコープに属する. | ○ | ○ | ☓ |
オブジェクトを通してのみ通常のメンバ関数が実行できる. | ○ | ☓ | ☓ |
カプセル化
クラスの各メンバは,生成されたオブジェクトや継承されたクラスからアクセス可能かどうかなどを指定できる.オブジェクト内部のデータや振る舞い,実際の型を隠蔽する行為はカプセル化と呼ばれ,オブジェクト指向の設計方針における重要な概念の1つである.
指定にはpublicやprivate,protectedキーワードを使用する.メンバはアクセス制限によって次のように呼ばれる.
オブジェクトの各メンバへのアクセス
クラスのオブジェクトの各メンバへのアクセスは,次の構文によって制御される.
実際の例を以下に示す.
class Hoge {
int x; // private
public:
int y; // public
Hoge(){} // public
~Hoge(){} // public
};
非公開メンバ | 限定公開メンバ | 公開メンバ | |
---|---|---|---|
他のメンバ関数 | ○ | ○ | ○ |
フレンド | ○ | ○ | ○ |
派生クラスのメンバ関数 | × | ○ | ○ |
クラスのオブジェクト | × | × | ○ |
コンストラクやデストラクタは,必ず公開されていなければならない.
また,C++が保護してくれるのは過失による不正アクセスのみであり,アドレス操作や型変換を用いた意図的な不正アクセスを検知できない.汎用言語の悪用からシステムを守れるのは,ハードウェアだけであり,現実のシステムではそれさえ厳しい.
基底クラスのアクセス制御
非公開派生 | 限定公開派生 | 公開派生 | ||||
---|---|---|---|---|---|---|
限定公開メンバ | 公開メンバ | 限定公開メンバ | 公開メンバ | 限定公開メンバ | 公開メンバ | |
派生クラスのメンバ関数 | ○ | ○ | ○ | ○ | ○ | ○ |
フレンド | ○ | ○ | ○ | ○ | ○ | ○ |
派生クラスを基底とする派生クラスのメンバ関数 | × | × | ○ | ○ | ○ | ○ |
任意の関数 | × | × | × | × | × | ○ |
多重継承(複数の基底クラスからの継承)である時,あるメンバに対するアクセス許可が1つでもあれば,そのメンバにアクセスできる.
また,アクセス指定子はいずれの場合も省略可能であるが,明示的に記述した方がソースコードは見やすくなる.
静的メンバ
static修飾子のつけて宣言されたメンバを静的メンバと呼ぶ.静的オブジェクトメンバの値は,クラスやそのオブジェクトで共有される(メモリ上の実体は常に1つだけしかない).静的メンバは,オブジェクトを生成しなくてもアクセス可能である(むろん公開部のみ).constでない静的メンバオブジェクトを宣言した場合,その定義はクラス定義の外側に記述しなければならない.constな静的メンバオブジェクトの場合,定数式の初期設定子によって初期設定できる.
class Hoge
{
public:
static int x;
const static int y = 10;
Hoge(){}
static void func(){ std::cout << "hoge\n"; }
};
int Hoge::x = 10;
int main()
{
Hoge::func();
return 0;
}
静的メンバは大域変数や名前空間内の変数と同様にmain関数の呼び出し前に生成と初期化が行われ,main関数が制御を返した後にデストラクタが呼び出される.ただし,ダイナミックリンクされるライブラリで定義された場合,生成や初期設定はリンクされた時である(関数内の静的変数は制御の流れが最初に通った時に初期化される).静的メンバや,グローバルスコープや名前空間スコープに属するオブジェクトは,ソースコード内の上の方に定義したものから順に初期化され,デストラクタはこの逆順に呼び出される.しかし複数のソースファイルを扱う場合,どのファイルを先に初期化するか,またはデストラクタを呼び出すかは言語処理系に依存する.
初期設定の文もデフォルトコンストラクタもない静的オブジェクトメンバは,その型の0(bool型ならfalse,char型ならnull文字といったぐらい)で初期化される.
定数メンバ
constで修飾されたメンバを定数メンバと呼ぶ.定数オブジェクトメンバは,複合代入を含む値の代入ができない(初期化は代入ではないことに注意).静的な定数オブジェクトメンバは右辺が定数式の初期設定子によって,静的でない定数オブジェクトメンバはメンバ初期化リストによって初期化できる.静的な定数オブジェクトメンバの初期設定を明示的に行わない場合は,値はその型の0になる.定数オブジェクトメンバは定数であるため,初期値が必須である.
class Hoge
{
public:
const int a; // ok
const int b = 2; // エラー
static int c; // ok 初期値:0
static const int d = 3; // ok
Hoge() : a(1) {}
};
定数(const)メンバ関数
メンバ関数は,メンバの値を変更しないこと明示的にを表せる.
class Hoge
{
public:
int x;
void change();
{
x = 100;
return;
}
void func() const;
}
void Hoge::func() const
{
x = 10; // エラー
change(); // エラー
return;
}
クラスでは,カプセル化によって見かけ上が不変であっても,実体が不変でない場合がある.例えば,あるメンバ関数が状態を記録する非公開オブジェクトを保持している場合である.このような場合,プログラマはその関数を論理的にメンバの値を書き換えないものとして扱うことができる.これを実装する例を2つ示す.
定数メンバ関数は,キャストによりconstを除去することで,実際にはメンバの値の書き換えができてしまうことがある.
class Hoge
{
private:
int x;
public:
void func() const
{
Hoge *p;
p = const_cast<Hoge*>(this);
p->x 10;
return;
}
}
mutable
記憶クラス指定子mutableを付けて宣言されたメンバは,定数メンバ関数であっても書き換え可能である.
class Hoge
{
private:
mutable int x;
public:
void func() const
{
x = 10;
return;
}
}
継承
クラス宣言の際,別のクラスの情報を引き継いで定義すること(継承)ができる.まず継承の文法を以下に示す.
class Human
{
};
class John : Human
{
};
基底クラスのコンストラクタは明示的に呼び出せる. 派生クラスのコンストラクタが明示的にコンストラクタを呼び出さない場合, コンストラクタは,基底クラスから順番に呼び出される.
デストラクタは,派生クラスから順番に呼び出される. 派生クラスのデストラクタが明示的にデストラクタを呼び出させない.
宣言の一致 エラー: 名前の隠蔽によるエラー
#include
class Hoge
{
public:
int foo(char c){return 1;}
Hoge(){}
~Hoge(){}
};
class Fuga : public Hoge
{
public:
int foo(char *c){return 0;}
Fuga(){}
~Fuga(){}
};
int main()
{
Fuga* p = new Fuga();
p->foo(1);
return 0;
}
多様性(ポリモフィズム)
複数のクラスを定義する時,共通の目的で同じ動作を行う関数を基底クラスで定義し,異なる目的で異なる動作を行う関数を派生クラスで定義すれば,それらの関係を簡潔に表現できる.しかし特定の状況では,共通の目的で異なる動作を行う例がある.C++では,このような場合に関数を基底クラスで定義し派生クラスで上書き(オーバーライド)することで,表すことができる.これにより,同じインタフェースでオブジェクトに応じて異なる処理を行える.このような性質を多様性(ポリモフィズム)または多態性などと呼ぶ.
例えば「剣」,「槍」というクラスを定義しようとする.これらの共通の性質を「武器」というクラスにまとめ,独立する性質をそれぞれのクラスで表現することで,簡潔な構造となる.「攻撃」という動作は,全ての武器で共通する性質だが,その振る舞いはそれぞれの武器で異なる. このような場合,次のように表現することができる.
class Weapon
{
public:
Weapon(){}
~Weapon(){}
virtual void attack(){return;};
};
class Sord : public Weapon
{
public:
Sord(){}
~Sord(){}
void attack(){ std::cout << "剣の攻撃\n"; return;}
};
class Lance : public Weapon
{
public:
Lance(){}
~Lance(){}
void attack(){ std::cout << "槍の攻撃\n"; return;}
};
int main()
{
Weapon *w = new Sord;
w->attack(); // Sord::attack()が呼び出される.
delete w;
w = new Lance;
w->attack(); // Lance::attack()が呼び出される.
delete w;
return 0;
}
多重定義
多重継承
C++では,クラスは複数の基底クラスを持つことができる.このような継承を多重継承という. また,
多重継承による曖昧さによるエラーの解決
実際の実行結果だと 基底クラスのコンストラクタの順番 (宣言順 左から右)
デストラクタ (宣言順 左から右)の逆
仮想基底クラス
継承で基底クラスを指定する際に,virtualキーワードを指定できる.
class Hoge
{
public:
int x;
Hoge(){}
~Hoge(){}
};
class Fuga : virtual public Hoge
{
public:
Fuga(){x=10;}
~Fuga(){}
};
class Piyo : virtual public Hoge
{
public:
Piyo(){x=100;}
~Piyo(){}
};
class Foo : public Fuga, public Piyo
{
public:
Foo(){x=1000;}
~Foo(){}
};
その他
thisポインタ
thisキーワードは,非静的メンバ関数の中で,メンバ関数の呼び出し対象のオブジェクトを指すポインタ.thisは変数ではない.それゆえ,thisのアドレスを変更することはできない.使用例を以下に示す.
class Hoge
{
public:
int i;
void func()
{
this->i;
return;
}
};