クラスはC++の要である。
class C {
int member;
void method() {
// なんちゃら
}
};
という風にメンバーとメンバ関数を定義できる。C++ではメンバ変数とメンバ関数というらしい。
さてこうして定義したクラスを
C c;
として実体が作られる。これは構造体も同じであり、そしてスタックに領域が確保される。
C* p = new C();
とか
C* p = new C;
とかすればヒープに確保されるわけだ。
そして
c.member = 10;
c.method();
p->member = 100;
p->method();
などとしてメンバを書き換えたりメンバ関数を実行したりできる。ちなみに上の定義ではこれは実行できない。なぜならば、何も指定しないとprivateとなってしまい、外部からアクセスできないためだ。
class C {
public:
int member;
void method() {
// なんちゃら
}
};
としてあげればアクセスできる。
class C {
public:
static int member;
static void method() {
// なんちゃら
}
};
とすれば
C::member = 10;
C::method();
のようにアクセスできる。
特殊なメンバ関数達
クラスを生成すると自動的に作られるメンバ関数がある。これらの機能を抑制するには上書きするか、宣言だけして中身を定義しないという方法がある。ただし、多くの場合はそれは好ましいことではないだろう。
コンストラクタ
コンストラクタは、クラスが作られたときに呼び出されるメンバ関数である。何も書かない場合はコンパイラが自動的に何もしないコンストラクタを生成する。コンストラクタの名前はクラスと同じにになる。また、戻り値は書かない。
class C {
public:
C() { // クラスメイト同じで戻り値は書かない
std::cout << "In C" << std::endl;
}
};
int main() {
C c;
std::cin.get();
return 0;
}
コンストラクタではクラスを生成する上で先だって初期化しなければならないメンバ変数を、初期化子リストで初期化できる。書き方は以下の通りで、コンストラクタの引数の後に:を付けて、続けてメンバ変数(式)と書く。複数ある場合は,で区切る。
class C {
public:
int x;
S& sr;
C(int value, S& sr) : x(value), sr(sr) {
std::cout << "In C" << std::endl;
}
};
初期化子リストは、クラスで宣言されている順序で行われる。初期化子リストの順序は関係がない。
struct A {
A() { std::cout << "A"; }
A(int x) { std::cout << "A(" << x << ")"; }
};
struct B {
B() { std::cout << "B"; }
B(int x) { std::cout << "B(" << x << ")"; }
};
とあったとしよう。
class C {
A a;
B b;
public:
C() : b(10), a(20) { }
};
はB(10)に続いてA(20)が実行される。なお、今回の場合、初期化子リストを省略した場合はA(), B()が実行される。(つまり宣言しただけでインスタンス化される。ポインタではないから当然である。)
継承された場合、その継承する順序によって初期化される順序が決まる。
class C : A, B {
public:
C() : B(), A() { }
};
はA(), B()の順序で行われる。
class C : B, A {
public:
C() : B(), A() { }
};
こちらはB(), A()の順序で行われる。初期化子リストの順序は関係がない。
class A {
public:
A() { std::cout << "A()" << std::endl; }
};
class B {
public:
B() { std::cout << "B()" << std::endl; }
};
class C {
public:
B b;
C() : b() { std::cout << "C()" << std::endl; }
};
class D : C {
public:
A a;
D() : a() { std::cout << "D()" << std::endl; }
};
この例では、
B() // Dの親のCのメンバbが初期化される
C() // Dの親のCが初期化される
A() // Dのメンバaが初期化される
D() // Dが呼び出される
の順に初期化される。つまり、クラスをたどり、そのコンストラクタの直前にメンバの初期化が宣言された順に実行される。
ムーブコンストラクタ
ムーブコンストラクタは、コンストラクタの引数として右辺値を渡したときに、その右辺値を破棄するが、使えるものは流用してコピーするコンストラクタである。C++11以降から利用可能である。
以下の例では、クラスCにムーブコンストラクタC(C&& it)を宣言して、ポインタpをそのまま流用している。newされたデータサイズが大きいときには、コピーが発生しないので高速に動作することが分かる。
実際にムーブするときには、std::moveをつかってオブジェクトを右辺値に変換する。(型が変わっただけである)そして、それをコンストラクタに引数として渡すか、コンストラクタの代入の式(例の通り)としてムーブコンストラクタを呼び出す。
struct C {
int* p;
C(int x) : p(new int(x)) {
cout << "create" << endl;
}
C(C&& it) : p(it.p) { // もとのpをそのまま流用
it.p = nullptr; // もとのpは使えなくする
cout << "move" << endl;
}
~C() {
delete p;
}
};
int main(void) {
C c(0);
*c.p = 10;
C c2 = move(c); // ここでムーブ
cout << c.p << endl; // このポインタの実体はない
cout << *c2.p << endl;
cin.get();
return 0;
}
C++11でSTLもムーブコンストラクタに対応したため、ムーブで十分な場合はこれらを使うことで高速に動作することが可能である。もちろん、従来のC++でもポインタを付け替える処理を何らかの方法で実装すれば可能であるため、新しい技術というわけではない。
デストラクタ
デストラクタはクラスのインスタンスがスコープを抜けるときに呼び出される。newされたものの場合はdeleteされたときに呼び出される。デストラクタの名前は「~クラス名」である。
class C {
public:
C() {
std::cout << "In C" << std::endl;
}
~C() {
std::cout << "In ~C" << std::endl;
}
};
int main() {
{
C c; // ここでコンストラクタが呼び出される
} // ここでデストラクタが呼び出される
std::cin.get();
return 0;
}
コピーコンストラクタ
コピーコンストラクタは他のクラスを使って初期化されるときに呼び出される。コピーコンストラクタは、コンストラクタの引数として自身の参照を受け取るように宣言する。以下の例ではコピーコンストラクタが呼び出されることが確認できる。尚、初期化時でないただの=では代入されるだけで、コピーコンストラクタは呼び出されないことに注意。
class C {
public:
C() {
std::cout << "In C" << std::endl;
}
C(C& self) {
std::cout << "In C(COPY)" << std::endl;
}
};
int main() {
C c; // In C
C c2 = c; // In C(COPY)
C c3(c); // In C(COPY)
c2 = c; // これは=演算子が呼ばれるだけでコピーコンストラクタは呼ばれない
std::cin.get();
return 0;
}
そのほか、関数の引数やラムダ式のコピー渡しとして渡す場合にも呼び出される。
代入演算子
代入演算子がないと、代入によってコピーができなくなる。コピーとは何だろうか? それは宣言されたデータのフィールドが全てコピーされると言うことである。それはint型でも任意の型でも、ポインタ型でも。これに対して不都合があれば自分で定義し直す必要がある。上の例では、c2 = c;で代入演算子が実行される。この中身は、cのフィールド全てをc2のフィールドにコピーする。
ところでクラスに参照があった場合どうしたらいいだろうか? 参照は、参照する先を変更しないことがC++のルールである。(もちろん、インラインアセンブリでごにょごにょすれば変更できないわけではない。中身はポインタであることが多いからだ) 答えは代入演算子が生成できない、である。コンパイラは警告を出すだろう。選択肢は2つ。自分で定義するか、=演算子を禁止するか、だ。
以下に敢えて参照を無視して代入演算子を定義した例を示すが、これが適切かどうかはクラスの設計者にしか分からない。今回は意味のないクラスだから、これが適切かどうか分からない。もしも参照先を移す必要があるならリファレンスint&を使うこと自体が誤りで、int* pなどポインタにするべきだろう。あるいは代入演算子の存在が誤りなのかもしれない。
class C {
public:
int x;
int& r;
C(int x, int& r) : x(x), r(r) {
std::cout << "create" << std::endl;
}
C& operator=(const C&& c) {
x = c.x;
std::cout << "move" << std::endl;
return *this;
}
void show() {
std::cout << "SHOW:" << x << ", " << r << std::endl;
}
};
int main() {
int x = 1;
int y = 2;
C c(10, x);
std::cout << x << ", " << y << std::endl;
c.show();
c = C(20, y);
std::cout << x << ", " << y << std::endl;
c.show();
std::cin.get();
return 0;
}
ほかにもnew, delete演算子も特殊なメンバ関数であるが、new, delete演算子はここでは割愛する。
包含による委譲
クラスA, Bがあって、クラスBがクラスAを使う場合を考えよう。
class A {
public:
void method() {
}
};
class B {
private:
A a; // BはAを包含
public:
void method() {
a.method(); // BはAに委譲
}
};
この場合、クラスBの中にクラスAのインスタンスがあって、クラスAに仕事を任せている。これを委譲という。包含する場合はほとんどの場合委譲が発生する。この包含による委譲は、この委譲先のインスタンスが変更することで大きなメリットとなる。というのも、静的な委譲(インスタンスが変わったりしない)の場合は、以下に述べるprivate継承を使って実現できるからである。private継承は、サブクラスと関係がなくなるため、いくら継承しても問題ない。継承より委譲を使え、という場合の委譲にもパターンがあるのだ。
継承・派生
継承は他のクラスの中身を引き継ぐ。次のコードは、クラスBを派生してクラスCを定義している。
class B {
int x;
};
class C : B {
// メンバxは継承される
};
継承と派生は混同されがちだが、C++では、クラスからそのクラスを引き継いで新しくクラスを作ることを派生するという。そして、元のクラスのメンバは継承されるという。
派生できなくすることもできる。
class B final {
};
class C : B {
};
とすればCはBを派生しようとするのでコンパイルエラーとなる。
コンストラクタが呼び出された時点で、初期化しなければならない場合がある。
class S {
public:
S(int x) { }
};
class C {
public:
S s;
C() { // sがS()で初期化されようとしているが引数が足りないためエラーとなる
}
};
こういった場合、コンストラクタの{から}のなかで初期化しようにもできないため、初期化子リストで初期化する。
class C {
public:
S s;
C() : s(10) {
}
};
派生した場合、派生元のコンストラクタも呼び出されるため、初期化が必要である。
class S {
public:
S(int x) { }
};
class C : public S {
public:
C() : S(100) {
}
};
C++11では自分のクラスの他のコンストラクタも呼び出せるようになった。
class S {
public:
S(int x) { }
};
class C {
public:
S s;
C() : C(100) {
}
C(int x) : s(4) {
}
};
派生したクラス、すなわちサブクラスを呼び出すと、コンストラクタが呼び出されるが、それよりも前に親のコンストラクタを呼び出す。そして、親が複数いる場合はクラスに並べられている順序で呼び出される。
class A {
public:
A() { std::cout << "A()" << std::endl; }
};
class B : A {
public:
B() { std::cout << "B()" << std::endl; }
};
class C : A, B {
public:
C() { std::cout << "C()" << std::endl; }
};
class D : A, B, C {
public:
D() { std::cout << "D()" << std::endl; }
};
は、D d;とすると次の結果を得る。//以降は実行結果にない、コメントである。
A() // まずAが呼ばれる
A() // 次にBの親であるAが呼ばれる
B() // そしてBが呼ばれる
A() // まずCの親であるAが呼ばれる
A() // 次にCの親であるBの親であるAが呼ばれる
B() // そしてCの親であるBが呼ばれる
C() // 最後に親であるCが呼ばれて
D() // Dが呼ばれる
private/protected/public継承
class クラス名 : (継承のアクセス指定子) 派生元のクラス名 { }
でクラスを宣言できる。アクセス指定子を省略するとprivateが指定される。ただし、structの場合はpublicな継承がされる。
private/publicな継承はそれぞれ異なった意味を持つ。具体的な特徴については、以下に述べるとおりであるが、そもそも使う場面が異なる。これについては後述する。
publicにすればクラスBの制約に基づいてアクセスできる。public継承をしても、派生元のメンバがprivateならそのメンバにはアクセスできない。ただし、派生元のメンバがprotectedだった場合はアクセス可能である。
class B {
public:
int x;
private:
int y;
protected:
int z;
};
class C : public B {
void method() {
x = 10; // アクセス可能
y = 20; // privateなのでエラー!
z = 30; // アクセス可能
}
};
int main() {
C c;
c.x = 100; // アクセス可能
c.y = 200; // privateなのでエラー!
c.z = 300; // protectedなのでエラー!
}
これは上記の通り、派生されたクラスCの中からクラスBのメンバxを書き換えようとしている。これは可能である。privateなyだと派生されたクラスであっても書き換えできない。
privateな継承は派生元のメンバ変数の内public/protectedのものにはアクセス可能である。これはpublicな継承と同じである。ただし、派生したクラスを使う側からは、派生元のクラスの全てのメンバにアクセスできない。つまり、以下のようなことである。
privateな継承の場合、派生先のクラスからでも(今回はクラスCの派生は自明なので省略している)、そのクラスを使う側からでもアクセスできない。
class B {
public:
int x;
private:
int y;
protected:
int z;
};
class C : private B {
void method() {
x = 10; // アクセス可能
y = 20; // privateなのでエラー!
z = 30; // アクセス可能
}
};
int main() {
C c;
c.x = 100; // 派生元のメンバ変数にはアクセスできない!
c.y = 200; // 派生元のメンバ変数にはアクセスできない!
c.z = 300; // 派生元のメンバ変数にはアクセスできない!
}
protectedな継承の場合は、派生先に限って派生元のpublic/protectedなメンバーにアクセスできる。つまり、以下のように中心となるクラスCがあって、このクラスCがクラスBをprotectedな継承をするとき、クラスCから派生するクラスDの中では、クラスBのpublic/protectedが指定されたメンバにのみアクセス可能である。
class B {
public:
int x;
private:
int y;
protected:
int z;
};
class C : private B {
void method() {
x = 10; // アクセス可能
y = 20; // privateなのでエラー!
z = 30; // アクセス可能
}
};
class D : public C {
void method() {
x = 100; // アクセス可能
y = 200; // privateなのでエラー!
z = 300; // アクセス可能
}
};
また、private/protectedな継承は、publicな継承のように自由ではない。次のようなクラスB, Cがあったとして、
class B { };
class C : B { };
B* b = &C();
これはできない。すなわち、派生元のクラスにキャストできない。これは後述するポリモーフィズムを実現できないことを意味する。
private継承の使い道
実は委譲・包含を使ったもの(の一部)は原理的に以下のprivate継承と同等である。
class A {
public:
void f1() {
}
void f2() {
}
void f3() {
}
};
class B : private A {
public:
void method() {
f1();
f2();
f3();
}
};
この場合、クラスBはクラスAのメソッドf1, f2, f3などを使って実装している。つまり複雑なクラスAを簡単なmethodにラップしてあげている。そして、クラスBを使う側からはprivateなクラスAを見られないという、委譲と全く同じ状態になっている。
何が異なるかと言えば、包含していないで直接クラスAにアクセスしていることである。publicなメンバにはアクセスできるが、privateなメンバにはアクセスできない。これは委譲の時も同じである。ただし、継承の場合、protectedなメンバにもアクセスできる。継承先で使って欲しい場合はprotectedにする。
多くは委譲で解決する問題であるが、private継承は、いわばクラスへのアクセサであると考えて欲しい。つまり、以下のように機能を絞って提供することができる。
class App : private A {
public:
void f1() {
A::f1();
}
};
なお、using宣言を使うことでprivate継承をしていても継承元を呼び出せる。
class App : private A {
public:
using A::f1();
};
App app;
app.f1(); // OK
純粋仮想メソッドの存在する抽象クラスをprivate/protected継承することができる。
class A {
public:
virtual void func() = 0;
void func2() { }
void func3() { }
};
class B1 : private A {
public:
void func() {
func2(); // 使える
}
};
class B2 : private A {
public:
void func() {
func3(); // 使える
}
};
ただし、ポリモーフィズムはできない。
B1 b1;
B2 b2;
A* a;
a = &b1; // privateな継承をするとここでエラー!
多重継承
class B1 {
public:
int x;
};
class B2 {
public:
int y;
};
class C : public B1, public B2 {
};
とあって、
C c;
c.x = 10;
c.y = 20;
とできる。多重継承は、複数の引き継ぎの他、主にインターフェイスの実装に使われると考えて良いだろう。そもそも継承のほとんどの利用方法は抽象クラス或いはインターフェイスの実装にあると言っても過言ではない。
抽象クラス
まずは抽象クラスを見てみよう。とはいってもC++には抽象クラスの機構がる訳ではなく、仮想関数という機構を用いる。仮想関数あるいは純粋仮想関数のあるクラスを抽象クラスという。
まずは普通のメンバ関数で考えてみよう。
class B {
public:
void method() {
std::cout << "In B" << std::endl;
}
};
class C : public B {
public:
void method() {
std::cout << "In C" << std::endl;
}
};
とあって、
C c;
c.method();
と実行した場合どちらが実行されるだろうか? 答えはCの方である。同様に、
B b;
b.method();
はBのほうが実行される。さて、いま
class C2 : public B {
public:
void method() {
std::cout << "In C2" << std::endl;
}
};
というクラスC2があって、
C c;
C2 c2;
と2つのインスタンスがあったとしよう。これらは共通な継承元Bというものがあるのだから、
B* b = &c;
b->method();
B* b2 = &c2;
b2->method();
とポインタでキャストしたとき、(1) クラスBのメンバ関数methodを実行したい (2) 元のサブクラスのメンバ関数C1::methodあるいはC2::methodを実行したいという2つのパターンがある。(1)は何もしなくてもその通りになるため実現できる。(2)についてはこれから述べる仮想関数という仕組みを使って実現できる。(2)のようなmethodの呼び出しができることをポリモーフィズム(多態性とも言う)という。これはオブジェクト指向の重要な特徴であるとされる。
仮想関数はvirtualキーワードを用いて定義できる。仮想関数は、元の実体のポインタが基底クラスにキャストされても実行される。
class B {
public:
virtual void method() {
std::cout << "In B" << std::endl;
}
};
class C : public B {
public:
void method() {
std::cout << "In C" << std::endl;
}
};
class C2 : public B {
public:
void method() {
std::cout << "In C2" << std::endl;
}
};
int main() {
C c;
C2 c2;
B* b;
b = &c;
b->method(); // In C
b = &c2;
b->method(); // In C2
std::cin.get();
return 0;
}
そしてこのようにvirtualが付いたメンバ関数を持つクラスは、CやC2からBクラスにキャストして、共通機能のmethodという名前で実行される。同じ名前なのに元のクラスのCやC2の関数が呼び出されるのだ!
上書きしない場合はvitualのクラスが呼び出される。もしも必ず上書きしないといけないことにしたければ、
class B {
public:
virtual void method() = 0;
};
と書けばいい。これは純粋仮想関数と呼ばれる。サブクラスでこのメンバ関数と同じシグネチャを持たないとコンパイルエラーとなる。純粋仮想関数を持つクラスを抽象クラスという。抽象クラスはコンストラクタによってインスタンス化できない。
なお、純粋仮想関数をコンストラクタ・デストラクタ内から呼び出してはならない。
class A {
public:
A() {
f();
};
virtual void f() const = 0;
};
class B : public A {
public:
void f() const { std::cout << "f" << std::endl; };
};
ここで、
B b;
としたらどうなるだろうか? コンストラクタは、A, Bの順序で呼び出されるため、Aの時点でfは仮想関数であり、Bのものが呼ばれるわけではないから、コンパイルエラーとなる。
仮想関数のデフォルト引数の挙動は気をつけた方がいい。
class A {
public:
virtual void f(int n = 1) {
std::cout << "a" << n << std::endl;
}
};
class B : public A {
public:
void f(int n = 2) {
std::cout << "b" << n << std::endl;
}
};
に対して次の処理はどう返すか?
B b;
A* a = &b;
b.f(); // これは"b2"
a->f(); // なんと、"b1"
virtualを指定すると、上書きされることが想定されるのでそこまではいいが、デフォルト引数は上書きされない! つまり、Bのfをそのまま呼ぶのではなく、Bのfを、Aのfのデフォルト引数で呼ぶという仕様になっている。
インターフェイス
インターフェイスとは、ある処理が必要とする機能をソースコード上で仕様として公開することで、処理対象がその機能を必ず実装することを契約し、異なる型でも同様の処理ができるようにするためのものである。
例えば、コンソール画面上に表示するためにクラスの内容を文字列に変換する機能をtoStringと名付ける。このtoStringは、様々なクラスに実装されていないと意味をなさない。つまり、showだったりto_stringだったりconvertToStringだったりと名前が異なってもいけないし、toString(T&)だったりtoString(const T&, string&)だったりしてもいけない。戻り値や引数も含めた全てが一致していないと意味がない。
そこでインターフェイスを次のように定義する。とはいってもC++にインターフェイスを表現する専用の文法は存在せず、クラスを使う。但し、中身は純粋仮想関数のみである。
#include <iostream>
#include <string>
using namespace std;
class I {
public:
virtual string toString() = 0;
};
class C : public I {
public:
string toString() {
return "C";
}
};
class C2 : public I {
public:
string toString() {
return "C2";
}
};
int main() {
cout << C().toString() << endl; // 安心してtoStringを使える
cout << C2().toString() << endl; // 安心してtoStringを使える
cin.get();
return 0;
}
この例ではtoStringを使う範囲を限定していなかったが、例えばクラスCが何らかの処理をするとする。このクラスCが委譲するクラスA, B, ... などはCとのインターフェイスを持つ必要が出てくる。そこでCとのインターフェイスを次のようにして定義すれば、A, Bと処理を分けることなくコードが書ける。
class C_IF {
public:
virtual void func() = 0;
};
class C {
public:
void method(C_IF& data) {
data.func(); // 安心してメソッドを呼べる
}
};
class A : public C_IF {
public:
void func() {
cout << "In A::func()" << endl;
}
};
class B : public C_IF {
public:
void func() {
cout << "In B::func()" << endl;
}
};
int main() {
A a;
B b;
C c;
c.method(a);
c.method(b);
cin.get();
return 0;
}
抽象クラスは、型を抽象化するのに対し、インターフェイスは機能を抽象化する点に注目されたい。
仮想デストラクタ
仮想関数は、継承するクラス(サブクラス)から継承されるクラス(基底クラス)へキャストした場合に、元のサブクラスの性質を維持したい場合に使う。Sub : Baseな関係があって、このSubをBaseと捉えたとき、元のSubの性質を持たせたいもの(これが仮想関数)には、Bのその性質にvirtualを付け、そうではなくBaseの性質を持たせたければvirtualを付けない。
デストラクタにもvirtualを付けることができる。付けた場合は、サブクラスをベースクラスにキャストして、そのまま開放(delete)した場合に、ベースクラスのデストラクタではなく、サブクラスのデストラクタが呼び出される。付けない場合は、2つのデストラクタが呼び出される。
class B {
public:
~B() {
std::cout << "~B()" << std::endl;
}
};
class C : public B {
public:
~C() {
std::cout << "~C()" << std::endl;
}
};
と定義されていたとき、
C* c = new C;
delete c; // ~C(), ~B()
B* b = new C;
delete b; // ~B()
一方、virtualを付けると、~B()はサブクラスCのデストラクタ~C()があればそちらを優先して実行するので、
class B {
public:
virtual ~B() {
std::cout << "~B()" << std::endl;
}
};
class C : public B {
public:
~C() {
std::cout << "~C()" << std::endl;
}
};
とした場合、
C* c = new C;
delete c; // ~C(), ~B()
B* b = new C;
delete b; // ~C(), ~B()
という風にどちらも~C()が実行される。なお、new/deleteを使わない場合は、スコープを抜けたときにデストラクタが呼び出されるだけなので、本体のサブクラス(C)がスコープを抜けない限りはデストラクタが呼ばれることはないため、virtualを付けても付けなくても挙動は変わらない。むしろ仮想関数のために無駄な処理が付け加わるだけである。
仮想基底クラス
次のソースを見て欲しい。これはBaseを継承したA, Bがあって、更にCはA, Bを多重継承している。このとき、元の基底クラスBaseのメンバxが2度呼び出されてしまい、Aのxなのか、Bのxなのか分からなくなってしまう。
曖昧さを解決するために、「c.A::x = 30;」や「c.B::x = 30;」と指定して書くこともできるが、そもそも複数基底クラスが存在していることが問題となることがある。もしも基底クラスが1つ存在するべきである場合はどうしたらよいだろうか?
class Base {
public:
int x;
};
class A : public Base {
};
class B : public Base {
};
class C : public A, public B {
};
int main() {
C c;
c.x = 30; // c.xは曖昧です
std::cin.get();
return 0;
}
これは、xがAの親であるBaseのメンバ変数なのか、Bの親であるBaseのメンバ変数なのか分からないからである。どちらか一方を区別する場合は、c.A::xやc.B::xなどと、そのメンバ変数名の前にスコープ演算子で場所を知らせる。
解決策として、Baseは常に1つだけ存在させるためにvitualキーワードを入れて、次のようにする。これでめでたくc.xにアクセスできた。
class Base {
public:
int x;
};
class A : virtual public Base {
};
class B : virtual public Base {
};
class C : public A, public B {
};
int main() {
C c;
c.x = 30; // c.xは曖昧です
std::cin.get();
return 0;
}
派生元、すなわち基底クラスのコンストラクターが引数を必要としているとき、その派生クラスのコンストラクターで基底クラスを呼び出さなかったらコンパイルエラーとなる。
class B {
public:
B(int x) {
std::cout << "x=" << x << std::endl;
}
};
class C : public B {
public:
C() : B(10) { } // 基底クラスのB(10)を呼び出している
};
class D : public C {
// 基底クラスCに引数が必要なコンストラクターが存在しないため省略できる
};
int main() {
C c;
D d;
std::cin.get();
return 0;
}
ここでBから派生するとき、virtualにするとどうなるだろうか?
class B {
public:
B(int x) {
std::cout << "x=" << x << std::endl;
}
};
class C : virtual public B {
public:
C() : B(10) { } // 基底クラスのB(10)を呼び出している
};
class D : public C {
// 規定のコンストラクタBを呼び出さないといけないとエラーとなる
};
int main() {
C c;
D d;
std::cin.get();
return 0;
}
結局、virtualというものは、多重継承とかそういうこと関係なしにvitualを付けて派生したクラス(C)を派生したクラス(D)に対して、vitualを付けて派生したクラス(C)でBのコンストラクタを呼び出さずに、直接Bのコンストラクタを呼び出す仕組みである。
メンバ変数へのポインタ
ポインタはポインタでもメンバ変数へのポインタというものがある。これは、変数そのものではなく、クラス内のメンバを指定するものである。
struct C {
int m;
};
とあったとき、
int* p = &c.m;
とすれば普通のポインタで、
int C::* mp = &C::m;
とすればメンバのポインタを受け取れる。
C c;
int C::* mp = &C::m;
c.m = 100;
std::cout << c.*mp << std::endl;
のようにしてメンバを取得できる。次の例では、2つのインスタンスに対し、メンバへのポインタを使ってそれぞれにアクセスしている。
struct C {
int m;
int n;
};
int main() {
C c1 = { 1, 2 };
C c2 = { 10, 20 };
int C::* p;
p = &C::m;
std::cout << c1.*p << std::endl; // 1
std::cout << c2.*p << std::endl; // 10
p = &C::n;
std::cout << c1.*p << std::endl; // 2
std::cout << c2.*p << std::endl; // 20
return 0;
}
なお、メンバ関数の時は
class C {
double func(int x) { }
}
に対して、
double (C::* pfunc)(int) = &C::func;
のように関数ポインタと同じ形を取る。
メンバへのポインタを使うのに有効な例が、後述するフレンド属性の継承である。
メンバ変数へのポインタと仮想関数
次の例では、Bの派生クラスCのインスタンスcに対して、c.B::funcと実行するのと、メンバ関数へのポインタに対して(c.*p)()と実行するのでの差を表したものである。
class B {
public:
virtual void func() {
cout << "In B::func()" << endl;
}
};
class C : public B {
public:
void func() {
cout << "In C::func()" << endl;
}
};
int main(void) {
C c;
c.B::func(); // In B::func()
(c.*&B::func)(); // In C::func()
cin.get();
return 0;
}
メンバ変数へのポインタは、仮想関数の場合、そのオブジェクトの上書きされた関数が呼び出される。
変更禁止
class C {
int x;
void f() const {
x = 100; // コンパイルエラー
}
};
とconstを付けることによりメンバ関数内でメンバ変数を変更できなくする。
class C {
int x;
void f() const {
g();
}
void g() {
x = 200;
}
};
こういうことが起きないように、上記の例ではコンパイルエラーとなる。呼び出すメンバ関数には全てconstが必要となる。
class C {
int x;
void f() const {
g();
}
void g() const {
}
};
なお、constが付いていないかどうかというだけでオーバーロードの選択対象になる。
class C {
int x;
void f() const {
g();
}
void g() const {
// こっちが実行される
}
void g() {
}
};
mutableキーワードで宣言すると、constメンバ関数なのにもかかわらず変更することができる。
class C {
public:
void f() const {
x = 10;
}
private:
mutable int x;
};
これはクラス外部から見て、メンバ変数xが変更されたとしても何ら変わりがない場合に使われる。
フレンド
フレンドクラス
フレンドクラスは、フレンドにしたいクラスに対してクラス内で
friend class クラス名
とする。すると、このクラスからprivateな自分のメンバを操作することができる。
class C {
friend class F;
int x;
public:
void f() {
std::cout << x << std::endl;
}
};
class F {
public:
F(C& c) {
c.x = 200;
}
};
int main() {
C c;
F f(c);
c.f(); // 200
std::cin.get();
return 0;
}
フレンド関数
この例ではクラスFのなかでc.xにアクセスしている。同様に関数単位でフレンドにすることもできる。以下の例では、互いに参照し合っているために前方で仮宣言を行っている。
class C;
class F {
public:
void func();
};
class C {
friend void F::func();
int x;
public:
void f() {
std::cout << x << std::endl;
}
};
void F::func() {
C c2;
c2.x = 100;
c2.f();
}
int main() {
F f;
f.func();
std::cin.get();
return 0;
}
テンプレートフレンド
テンプレートを使っていると、自身のクラスのメンバにアクセスできない場面がある。
template<class T>
class C {
private:
T x;
public:
C(int x) : x(x) { }
template<class S>
void f(C<S>& c) {
c.x = 10;
}
};
int main() {
C<int> c1(10);
C<double> c2(20); // C<double>だと……
c2.f(c1); // C<double>の中からC<int>のc.xは変更できないとエラーになる
return 0;
}
自身のクラスでも、いかなるテンプレートでも変更できるようにするためにテンプレートフレンドがあり、次のように書く。
template<class T>
class C {
template<class> friend class C;
private:
T x;
public:
C(int x) : x(x) { }
template<class S>
void f(C<S>& c) {
c.x = 10;
}
};
template<class> friend class C;
は、任意の型Uに対して、クラスC<U>をフレンドとして、クラスC<U>の内部からはこの定義のクラスC<T>にアクセスできることを意味する。すなわち、多対多の関係でフレンドになっている。
先ほどの状況通り、以下のようなクラスCとそのお友達Fを考えてみよう。
// Fの定義
template<class S, class T, class U>
class F {
public:
void func();
};
// Cの定義
template<class T>
class C {
friend void F<int, int, int>::func();
private:
T x;
public:
C(T x) : x(x) { }
};
// F::funcの実体は以下で定義
template<class S, class T, class U>
void F<S, T, U>::func() {
C<int> c(10);
c.x = 20;
}
friend void F<int, int, int>::func();
これはF<int, int, int>のメンバ関数funcをフレンド関数にしたもの。フレンドクラス内ではCへのアクセスが可能である。
template<class S, class T, class U>
friend void F<S, T, U>::func();
これはintだけでなく何でも受け取れるようにしたもの。
template<class S, class T, class U>
friend class F;
すべてのテンプレートに対してフレンドとなる。
template<class S>
friend F<S, int, int>;
このような特殊化はできない。(できて欲しいが)
フレンド属性の継承
class F;
class C {
friend F;
void func() { }
};
class F {
void mthod(C c) {
c.func();
}
};
このようなクラスでは、フレンドクラスFはフレンドであるCのprivateなメンバにアクセスできる。これはいい。ところが、このFを継承してできたFSubクラスを作ると、FSubからprivateなメンバにアクセスできない。どうしたらいいか?
改善案の一つとして、使えるクラスFの中で実行するようにラッピングしてあげる方法である。
class F {
public:
void _func(C& c) { c.func(); }
};
class FSub : F {
void method(C c) {
_func(c);
}
};
ただしこの方法では引数に実行するオブジェクトを入れる必要があり美しくない。これを解決するためにメンバ変数へのポインタを使う。
class F {
public:
F() : func(&C::func) { } // メンバへのポインタの初期化
protected:
void(C::*func)(); // メンバへのポインタ
};
class FSub : F {
void method(C c) {
(c.*func)(); // 実行できる!
}
};
演算子のオーバーロード
これは大変変態的な機能である。そう、演算子を自分で定義してしまおうじゃないかという発想。まずは単項演算子の例。もちろん戻り値を返すのが普通である。後置インクリ(でくり)メントは++(int)のようにint型を指定する。
class C {
public:
void operator +() {
std::cout << "単項+" << std::endl;
}
void operator -() {
std::cout << "単項-" << std::endl;
}
void operator *() {
std::cout << "単項*" << std::endl;
}
void operator ++() {
std::cout << "++単項" << std::endl;
}
void operator --() {
std::cout << "--単項" << std::endl;
}
void operator --(int) {
std::cout << "単項--" << std::endl;
}
void operator ++(int) {
std::cout << "単項++" << std::endl;
}
void operator &() {
std::cout << "単項&" << std::endl;
}
void operator !() {
std::cout << "単項!" << std::endl;
}
void operator ~() {
std::cout << "単項~" << std::endl;
}
};
int main() {
C c;
+c;
-c;
*c;
++c;
c++;
--c;
c--;
!c;
~c;
&c;
std::cin.get();
return 0;
}
続いて2項演算子を見ていこう。
class C {
public:
void operator +(int x) {
std::cout << "2項+" << x << std::endl;
}
void operator -(int x) {
std::cout << "2項-" << x << std::endl;
}
void operator *(int x) {
std::cout << "2項*" << x << std::endl;
}
void operator /(int x) {
std::cout << "2項/" << x << std::endl;
}
void operator %(int x) {
std::cout << "2項%" << x << std::endl;
}
void operator <(int x) {
std::cout << "2項<" << x << std::endl;
}
void operator >(int x) {
std::cout << "2項>" << x << std::endl;
}
void operator <=(int x) {
std::cout << "2項<=" << x << std::endl;
}
void operator >=(int x) {
std::cout << "2項>=" << x << std::endl;
}
void operator ==(int x) {
std::cout << "2項==" << x << std::endl;
}
void operator !=(int x) {
std::cout << "2項!=" << x << std::endl;
}
void operator &&(int x) {
std::cout << "2項&&" << x << std::endl;
}
void operator ||(int x) {
std::cout << "2項||" << x << std::endl;
}
void operator <<(int x) {
std::cout << "2項<<" << x << std::endl;
}
void operator >>(int x) {
std::cout << "2項>>" << x << std::endl;
}
void operator &(int x) {
std::cout << "2項&" << x << std::endl;
}
void operator |(int x) {
std::cout << "2項|" << x << std::endl;
}
void operator ^(int x) {
std::cout << "2項^" << x << std::endl;
}
void operator ,(int x) {
std::cout << "2項," << x << std::endl;
}
void operator [](int x) {
std::cout << "2項[]" << x << std::endl;
}
void operator ()(int x) {
std::cout << "2項()" << x << std::endl;
}
void operator=(const C &obj) {
std:cou:t << "2項=" << std::endl;
}
};
int main() {
C c;
c + 100;
c - 100;
c * 100;
c / 100;
c % 100;
c < 100;
c > 100;
c <= 100;
c >= 100;
c == 100;
c != 100;
c && 100;
c || 100;
c << 100;
c >> 100;
c & 100;
c | 100;
c ^ 100;
c, 100;
c[100];
c(100);
C c2;
c = c2;
std::cin.get();
return 0;
}
このようにほとんどの演算子を再定義できる。
なお、+=などは代入演算子との複合と解釈するのではなく、+=という2項演算子が存在する。従って、上記の例にはないが-=とか>>=とかどれもオーバーロード可能である。
変換関数といって、キャストするときに呼び出される関数も定義できる。次の例はキャストしたら100を返すようにした。
class C {
public:
operator int() {
return 100;
}
};
int main() {
C c;
std::cout << (int)c << std::endl;
std::cin.get();
return 0;
}
演算子のメソッドを呼び出したい場合は、operator演算子名(引数)とする。例えば、クラスAの代入演算子を表すには、A::operator=(x)とす。
インナークラス
クラスの中にクラスを定義できる。その場合、名前空間がクラス内にできていると見なされ、外部から内部のクラスにアクセスするには外側のクラスの名前を使って(外側)::(内側)としなければならない。
更にもう一つ特徴があって、内部のクラスは外部のクラスのprivateなメンバにアクセスできる。
class C {
private:
int x;
public:
void show() {
std::cout << x << std::endl;
}
class In {
public:
void f(C& c) {
c.x = 100; // 内部から外部のクラスにアクセスできる
}
};
};
int main() {
C::In inner;
C c;
inner.f(c);
c.show();
std::cin.get();
return 0;
}
なお、内部のクラスはpublicになっていないと外部に公開されない。
ローカルクラス
インナークラスのように関数の中にクラスを書ける。
int main() {
{
class C {
public:
int x;
};
C c;
c.x = 200;
}
C c2; // コンパイルエラー
std::cin.get();
return 0;
}
この場合は、宣言されたスコープを抜けるとアクセスできなくなる。
暗黙の型変換とexplicit
まずは次のたらい回し関数の遅延評価の例を見てみよう。
class tarai {
const int x, y;
const tarai& r;
public:
tarai(int z)
: x(z), y(z), r(r) { }
tarai(int x, int y, const tarai& r)
: x(x), y(y), r(r) { }
operator int() const {
return (x <= y) ? y :
tarai(tarai(x - 1, y, r),
tarai(y - 1, r, x),
tarai(r - 1, x, y));
}
};
このtaraiクラスは、コンストラクタをtarai(1, 2, 3)のように呼び出せる。コンストラクタはintを1つ取るtarai(int z)とint2つとtaraiの参照を取るtarai(int x, int y, const tarai& r)しかなさそうだが実行できてしまう。
からくりはtarai(int z)である。C++では、引数を1つだけ取るコンストラクタは特別に変換コンストラクタという。そして、変換コンストラクタは明示的呼び出しと暗黙的呼び出しの2種類あって、以下のように呼び出せる。
tarai t(10); // 明示的
tarai t = 10; // 暗黙的
話を単純にするために、
class C {
private:
int x;
public:
C(int x) : x(x) {
}
void f(C c) {
c.show(); // 30
}
void show() {
std::cout << x << std::endl;
}
};
を考えよう。このCのコンストラクタは引数を1つだけとるものがあるから、変換コンストラクタを持つことになる。そして暗黙的な変換がされることから、
C c1 = 10;
C c2(20);
c1.f(30); // こんな呼び出しもまかり通る
コンストラクタが=で呼び出せる。これは代入ではないことに注意されたい。コンストラクタが呼び出されることと等価である。従ってoperator=をオーバーロードしても呼び出されない。
さて、たらい回し関数に話を戻すと、「const tarai& r」に第3引数が暗黙的な変換によってintからこれにキャストされる。そしてtaraiの定義が終わり、この結果を得ようと(int)とキャストするときに計算が実行される。
rはzの代わりとなっている。rは(int)でキャストされる際に、x = y = zの値に変換される。このtaraiは一見関数のように表現できるが、じつはtarai自体では計算していなく、rがキャストされるときだけ計算している。例えばtarai(10, 5, 0)の場合、普通の再帰として定義した場合は343073回tarai関数が呼び出される。一方で、上記の実装だと126回しか呼び出されない。
このように、必要なときになって計算する方法を遅延評価という。C++には遅延評価をサポートする文法はないが、工夫することで上記のように計算できる。詳細は他で取り上げる。
話がそれたが、この変換コンストラクタを定義した際に、暗黙の変換をさせないようにするためにexplicitキーワードがある。
class C {
private:
int x;
public:
explicit C(int x) : x(x) { // 暗黙の変換がされなくなる
}
};
using宣言
using宣言を行うことでアクセス権限の昇格・降格ができる。
class Base {
protected:
int x, y, z; // いずれもサブクラスでアクセス可能
};
class Sub : protected Base {
public:
using Base::x; // サブクラスではpublicでアクセス可能; 昇格
private:
using Base::y; // サブクラスではprivateでアクセスする; 降格
};
class SubSub : public Sub {
public:
void func() {
x = 10; // 例えprivate継承でもOK
y = 20; // 降格したのでNG
z = 30; // protected継承だからOK
}
};
int main() {
SubSub subsub;
subsub.x = 10; // 昇格&public継承だからOK
subsub.y = 20; // 降格したからNG
subsub.z = 30; // protectedメンバだからNG
return 0;
}
usingを基底クラスのコンストラクタに適応する、継承コンストラクタが規格では存在しているが、VC++12では実装されていないため割愛する。
最終更新:2014年03月05日 14:48