ATLAS日本基礎ネットワーク C++トレーニングコース
ここでの目標
目次
クラスとはユーザが定義する型です。型とはそれに属するオブジェクトを作る枠のことです。これまでの型は組込型でした。整数とか浮動小数点数とか、C++にもともと用意されているものでした。その型に対する演算も組み込まれていました。
これからはクラスを使ってユーザが自由に型を加えていくことが出来ます。型、すなわちオブジェクトが持つべき形を決めると同時に、オブジェクトと他の型のオブジェクトの間の演算を定義することが出来ます。さらに、メソッドと呼ばれる、オブジェクトに固有の関数を定義し、プログラムの中でのオブジェクトの振る舞い方を規定することが出来ます。プログラムは、モデルとしては、いくつかの型のオブジェクトが相互作用しながらタスクを実現していくことになります。
クラスはメンバーデータとメンバー関数によって構成されます。クラスの定義では、そのクラスにはどのようなメンバーデータとメンバー関数があり、それぞれどのように使えばよいかを記述します。クラス定義は例えば次のような形になります。
class MyHistogram { public: MyHistogram( int nbins ); ~MyHistogram( ); void clear( ); void fill( double fvalue ); void dump( std::ostream& os ); private: int m_binsize; double * m_store; };
ここでMyHistogram
クラスを定義しています。(見るところ1次元のヒストグラムを扱うもののようです。)クラスは型定義であり、{}
にメンバーを記述した後;
で終端することで、クラス定義を完了しています。メンバーはpublic:
とラベルされたところに記述されたものと、private:
とラベルされたところに記述されたものがあります。public
なメンバーは関数のようです。private
なメンバーは変数になっています。
メソッドとも呼ばれるメンバー関数を見ていきましょう。MyHistogram
と~MyHistogram
は少し見慣れないかもしれません。クラス名と同じ名前の関数です。二つめのものは~
(1の補数・ビット反転)の演算子がついているように見えます。これらはコンストラクタ(構築子)、デストラクタ(破棄子)と呼ばれる関数で詳細は後述します。それ以外は何となく雰囲気がわかります。clear
でき、fill
でき、dump
できるようです。
メンバーデータの方はprivate
と宣言され名前はいずれもm_
で始まっています。これはATLASのコンベンションで、メンバーデータは(静的メンバーをのぞき)m_
で始まる名前を持ちます。静的メンバーはs_
で始まる名前を持つことになっています。
実際にクラスを定義するファイルを作りましょう。このクラス定義ファイルは2つの用途があります。一つはクラスを供給する側がメンバー関数やメンバーデータを定義するために必要です。もう一つはこのクラスの利用者がこのクラスのオブジェクトにアクセスするためにインターフェースとして必要になります。そのため、メンバー関数の定義ファイルなどとは分けて、ヘッダーファイルとして用意します。
今日の作業場所を用意します。
cd ~/tutorial/cplusplus
mkdir l4
cd l4
ATLASのコンベンションではクラスの名前に.h
をつけたものをヘッダーファイルの名前とします。今の例ではMyHistogram.h
ということになります。実際にMyHistogram.h
を作ってみましょう。
#ifndef __MYHISTOGRAM_H #define __MYHISTOGRAM_H #include <iostream> class MyHistogram { public: MyHistogram( int nbins ); ~MyHistogram( ); void clear( ); void fill( double fvalue ); void dump( std::ostream& os ); private: int m_bincount; double * m_store; }; #endif
内容は先ほどと同じなのですが、最初と最後におまけが付いています。#ifndef
はもし未定義ならば、というプリプロセッサ命令です。#define
で__MYHISTOGRAM_H
を定義し、#endif
までを読み込みます。もし__MYHISTOGRAM_H
が定義済みであればそれ以降#endif
までの部分は読み込まれません。この仕組みによって、MyHistogram
クラスの定義が二度出現することを防止しています。この仕組みをインクルードガードと呼びます。
一つのファイルの中でヘッダーファイルが二度呼ばれることはないと思いますが、他のヘッダーファイルがその中でさらにヘッダーファイルを読み込むということが頻繁にあり、どこかで同じファイルを読み込むことになると言うことはよくあります。そのためにインクルードガードは必要です。
#define
でマクロを定義する唯一のケースです。
クラスはあくまでも型です。整数型のオブジェクトを定義して変数として使うように、プログラム中ではそのクラスのオブジェクトを定義してそれを使います。
クラスのオブジェクトを作るためには、その段階までにクラスが定義されていなければなりません。クラス定義は、通常そのクラス名に.h
をつけたヘッダーファイルに記述されます。それ故、クラスを利用する最初の手順は#include
でクラス定義ヘッダーを読み込むことです。
#include "MyHistogram.h"
このヘッダーファイルの中でクラスMyHistogram
は定義されていますから、この後の文脈でMyHistogram
のオブジェクトを定義することが出来ます。
MyHistogram h( 100 );
ヒストグラムオブジェクトh
が定義されました。関数呼び出しはコンストラクタに対するものです。nbins
に対して100
という値を与えました。100
ビンあるヒストグラムになるはずです。後は、オブジェクトh
のメソッド(メンバー関数)を呼び出します。
h.clear( ); for( int i = 0; i < 100; i ++ ) h.fill( double( i ) ); h.dump( std::cout );
メソッドの指定はオブジェクト名h
の後に.
(ピリオド:メンバー選択演算子)を置いて、それに続けてメンバー関数を指定します。
happ1.cxx
として実際にファイルを書いてみましょう。
#include "MyHistogram.h" #include <iostream> main( ) { using namespace std; MyHistogram h( 100 ); h.clear( ); for( int i = 0; i < 100; i ++ ) h.fill( double( i ) ); h.dump( cout ); }
これをコンパイルしてみます。まだMyHistogram
クラスはヘッダーファイルを書いただけで実装をしていませんので実行することはできませんが、コンパイルが成功するかどうかは確認できます。やってみましょう。g++
に-c
オプションをつけて、コンパイルだけで終了します。
g++ -c happ1.cxx
エラーがなければ何も表示されずに終わるはずです。ls
で何ができたか確認しましょう。
ls
happ1.cxx happ1.o MyHistogram.h
happ1.o
というファイルができています。
この例では#include
が二回出てきます。"MyHistogram.h"
と<iostream>
です。"
で囲まれた方は現在の作業ディレクトリからファイルを探します。一方<>
の方は標準インクルードディレクトリから探します。この区別はATLASでは必須です。
標準インクルードディレクトリは
/usr/include/c++/4.4.7
です。g++
の-I
オプションでインクルードディレクトリを指定することもできます。
お気づきかもしれませんが、実はMyHistogram.h
の中にも#include <iostream>
が書かれています。happ1.cxx
では明示的にstd::cout
を使うので他のヘッダーファイルに依存せずにここには#include <iostream>
を書くべきです。インクルードガードのおかげで全く問題は生じません。MyHistogram.h
に書いてあるからここでは書かないというのはプログラムを理解しにくくします。
もう一つのオブジェクトの利用の仕方を見ましょう。new
演算子を使って動的にオブジェクトを割り付ける方法です。
MyHistogram * hp = new MyHistogram( 100 );
これはMyHistogram
型へのポインターhp
を定義し、それに初期値を与えています。new
演算子でMyHistogram
型オブジェクトを構築します。new
演算子は結果として構築されたオブジェクトのアドレスを返します。hp
はその値で初期化されます。これを操作しましょう。
hp -> clear( ); for( int i = 0; i < 100; i ++ ) hp -> fill( double( i ) ); hp -> dump( std::cout );
ポインターに作用するメンバー選択演算子->
を使用します。最初の例hp->clear();
は
(*hp).clear( );
と書くのと同じです。この書き方では演算順序では間接参照*
の優先順位がメンバー選択.
のそれより低いので括弧に入れる必要があります。煩わしいので->
という演算子を導入しました。
MyHistogram h;
として生成されたオブジェクトはその文脈で寿命が決まります。関数の中で定義されたオブジェクトはその関数から制御が離れるときに寿命が終わります。一方new
で生成されたオブジェクトはdelete
されるときもしくはプロセスが終了するときまで寿命を継続します。
delete hp;
で明示的に破棄します。
関数内に定義されたオブジェクトは一時的な記憶場所であるスタックに作られます。一方、new
による構築で使われるメモリー領域はヒープと呼ばれ、別に管理されています。ヒープに作られたオブジェクトが破棄されると、その領域はヒープに戻され、別のオブジェクトの記憶域として再利用されます。ポインターの持つアドレスをコピーして使ったりすると、オブジェクトは既に破棄されているのに、ポインターの値だけが残っていて、それが旧オブジェクト領域をアクセスしようとすると予期せぬことが起こってしまいます。new
で生成したオブジェクトの管理は特に慎重に行う必要があります。
オブジェクトの生成方法は2つあるわけで、
new
による方法がそのように危険なのであれば使わないようにすればよいと思うかもしれません。しかし、new
という演算子が用意されていると言うことはこれを使わないと実現できない機能があるからなのです。これについては次回説明します。
やってみましょう。happ2.cxx
を書きます。
#include "MyHistogram.h" #include <iostream> main( ) { using namespace std; MyHistogram * hp = new MyHistogram( 100 ); hp -> clear( ); for( int i = 0; i < 100; i ++ ) hp -> fill( double( i ) ); hp -> dump( cout ); delete hp; }
これも同様にコンパイルします。
g++ -c happ2.cxx
ここまで二つのクラス利用例を見てきましたがクラスメンバーの実装そのものはまだ行われていないことに注意してください。クラスの利用者はそのクラスのヘッダーさえ用意されればそれでプログラム開発ができます。クラスヘッダーはそのクラスの仕様書の役割をします。クラスの実装がどのように変わってもそのクラスの仕様書(クラスヘッダー)が変わらなければ利用者側はプログラムを書き換える必要がありません。開発者と利用者の隔離がクラス導入の重要なメリットの一つです。
クラスはその構成物としてデータと関数を持ち、それぞれ、メンバーデータ、メンバー関数(メソッド)と呼ばれます。C言語で実装された構造体struct
は、メンバーデータだけを持ったクラスと考えることが出来ます。この場合、データにアクセスする方法(メソッド)が用意されていないので直接メンバーデータに読み書きを施しました。クラスの場合、メソッドが用意されているので、クラスの利用者が直接メンバーデータをアクセスしなくても良くなりました。このことは画期的なことでした。つまり、
ということで、仕様と実装を分離することが可能になり、より大規模なプログラム開発が効率的に行えるようになりました。この仕組みを実装のカプセル化と呼びます。
ここでわかるように、利用者には一部のメンバー関数を公開し、メンバーデータやその他のメンバー関数を非公開にすることが期待されます。先の例でもpublic
なのはメンバー関数だけであり、メンバーデータはprivate
に置かれました。
メンバーデータには任意の型のオブジェクトを定義することが可能です。これまで見てきたような組込型(long
やdouble
など)はもちろん、他のクラスのオブジェクトをメンバーデータとして持つこと、オブジェクトへのポインターを持つことももちろん可能です。
ATLASではメンバーデータの名前はm_
で始まらなければなりません。また、すべてのメンバーデータはpublicにしてはいけません。
先ほどの例では
private: int m_bincount; double * m_store;
の部分です。
静的なメンバーデータを定義することも出来ます。静的なメンバーデータはクラスのオブジェクトの生成・破棄と無関係に、最初から最後まで存在します。そして、その間、データを保持し続けます。また、そのクラスのすべてのオブジェクトの間で値を共有することになります。
class StaticMemberClass { private: static int s_state; };
この静的なオブジェクトのために記憶域を定義しておく必要があります。
int StaticMemberClass::s_state = 0;
これは通常メンバー関数を記述するファイルに実装としておかれます。後で述べますが、StaticMemberClass::
はStaticMemberClass
というクラスに属するという意味です。
ATLASでは、静的なメンバーの名前はs_
で始まらなければなりません。
メンバー関数はクラスの振る舞いを規定します。上で説明したように、利用者は公開されたメソッドすなわちメンバー関数を見て、そのクラスの役割を理解します。
クラス定義がクラス名.h
ファイルに記述されたように、メンバー関数の定義はクラス名.cxx
ファイルに記述されます。(これはATLASのコンベンションです。) 上述の例によれば、MyHistogram.cxx
というファイルを用意することになります。その最初の部分は
#include <MyHistogram.h>
となるでしょう。クラス定義を読み込みます。その中で
void MyHistogram::clear( ) { int i; double * p = m_store; for( i = 0; i < m_binsize; i ++ ) * p ++ = 0.0; }
MyHistogram::
という指定がある以外は大域的な関数の定義と同じです。関数に引数を与えることが出来ますし、戻り値を持ちます。局所変数を使うことも出来ます。関数内で定義された局所変数のライフタイムは大域関数の場合と同じです。
メンバー関数がstatic
であることを宣言できます。通常のメンバー関数はそのオブジェクトに関して呼び出すのが普通ですが、static
なメンバー関数はそのクラスのオブジェクトを生成しなくても直接呼び出すことが出来ます。
class StaticExample { public: static int getObjectCount( ); };
関数getObjectCount
を直接呼び出したいときは
int iobj = StaticExample::getObjectCount( );
等とします。
関数がconst
であることを宣言できます。メンバー関数宣言の後にconst
を指定します。そのメンバー関数が呼ばれても、その関数の中でメンバーデータを変更しないと言うことを表します。
class ConstExample { public: int getEntryCount( ) const; };
const
が指定されている関数の実装(関数定義)でメンバーデータを変更しようとするとエラーになります。
内部状態を変えない関数の例としては、オブジェクトの内部情報を取り出すものがあります。ゲッターメソッドと呼ばれます。逆に、オブジェクトの内部情報〜パラメータをセットするための関数はセッターメソッドと呼ばれます。現在のC++では次に述べるコンストラクタとデストラクタ以外は文法的にメンバー関数を分類する仕組みはありませんが、機能的には分類可能です。クラスの設計を寄り合理的にするためにメンバー関数のカテゴリー分けをすることは有効です。
通常のメンバー関数は、それを利用するものが呼び出すことで初めて制御がそこへ移ります。C++のクラスではプログラマーが指定しないにもかかわらず暗黙に呼び出される二つのメンバー関数があります。クラスのオブジェクトのライフタイムを管理する上で必要なものなので、クラスの開発者がそれを記述しないと、コンパイラが自動的にデフォルトの関数を採用します。その二つのメンバー関数とはコンストラクタ(構築子)とデストラクタ(破棄子)です。
コンストラクタはそのクラス名と同じ名前の関数として記述されます。一方、デストラクタは~
を前につけ、続けてクラス名と同じ名前をおいた関数として記述されます。先の例ではコンストラクタがMyHistogram()
、デストラクタは~MyHistogram()
になります。コンストラクタもデストラクタも戻り値を持ちません。
コンストラクタは引数を持つことが出来ますが、デストラクタは出来ません。関数オーバーロードの仕組みにより、コンストラクタはいくつも実装できますが、引数をもてないデストラクタは一つしか実装できないことになります。
あるクラスのオブジェクトに関して、生成時にコンストラクタが呼び出され、破棄時にデストラクタが呼び出されると言うことは、コンストラクタの中で動的にメンバーオブジェクトが割り付けられ、デストラクタの中でそれが解放されることを想定しているとも考えられます。
MyHistogram::MyHistogram( int nbins ) { m_bincount = nbins; m_store = new double[ nbins ]; }
m_store
にdouble
型の記憶域がnew
で割り付けられました。new
で割り付けられた領域は明示的にdelete
で解放される必要があります。デストラクタの出番です。
MyHistogram::~MyHistogram( ) { delete [] m_store; }暗黙に呼び出されるデストラクタが正しく動的に割り付けられた記憶域を解放することは重要です。頻繁に生成消滅を繰り返すオブジェクトが作られるクラスで、デストラクタが記憶域の解放をさぼると、どんどん記憶域を消費していくことになります。オブジェクトが消滅して
m_store
というポインターがなくなったにもかかわらずdelete
でシステムに正しく戻されなかった記憶域は、もうそのプログラムの中では制御することが出来ません。こうしてメモリーがこぼれて減っていくことをメモリーリークと呼びます。
コンストラクタによるメンバーの初期化の方法として、メンバーデータの初期化子を関数定義に加える方法があります。上述の例で
m_bincount = nbins;
は代入文として初期化作業が実装されていますが、これは
MyHistogram::MyHistogram( int nbins ) : m_bincount( nbins ) {
という風に書いて初期化することも出来ます。ここでm_bincount
は整数型の変数ですが、( nbins )
と、関数呼び出しのように記述することで組込型int
のコピーコンストラクタを呼び出しています。この初期化は{}に制御が渡される前に行われるため、{}
では既に初期化済みの変数として使用することが出来ます。
ここまででMyHistogram
クラスを実装するために必要な情報はすべて集まりました。実装しましょう。MyHistogram.cxx
を書きます。
#include "MyHistogram.h" MyHistogram::MyHistogram( int nbins ) { m_bincount = nbins; m_store = new double[ nbins ]; } MyHistogram::~MyHistogram( ) { delete [] m_store; } void MyHistogram::clear( ) { double * p = m_store; for( int i = 0; i < m_bincount; i++ ) * p++ = 0.0; } void MyHistogram::fill( double fvalue ) { int bin = int( fvalue ); if( bin >= 0 && bin < m_bincount ) m_store[ bin ] += 1.0; } void MyHistogram::dump( std::ostream& os ) { for( int i = 0; i < m_bincount; i++ ) { if( i % 10 == 0 ) { os.width( 4 ); os << i << ": "; } os.width( 6 ); os << m_store[ i ]; if( i % 10 == 9 ) os << std::endl; else os << " "; } os << std::endl; }
まずコンパイルします。
g++ -c MyHistogram.cxx
続いて、先ほど作ったhapp1.o
とリンクして実行してみます。
g++ happ1.o MyHistogram.o ./a.out0: 1 1 1 1 1 1 1 1 1 1 10: 1 1 1 1 1 1 1 1 1 1 20: 1 1 1 1 1 1 1 1 1 1 30: 1 1 1 1 1 1 1 1 1 1 40: 1 1 1 1 1 1 1 1 1 1 50: 1 1 1 1 1 1 1 1 1 1 60: 1 1 1 1 1 1 1 1 1 1 70: 1 1 1 1 1 1 1 1 1 1 80: 1 1 1 1 1 1 1 1 1 1 90: 1 1 1 1 1 1 1 1 1 1
ヒストグラムの実装についてはもっともっと気の利いたものがありそうですね。
ある条件が整うとクラスのコンストラクタが自動的に作られる場合があります。それはデフォルトコンストラクタと呼ばれます。
あるクラスのコンストラクタが全く明示的には定義されていない場合、引数無しのコンストラクタが自動で生成される場合があります。そのコンストラクタではメンバーデータの型に応じそれぞれ引数無しのコンストラクタを呼び出します。
メンバーデータにconst
なメンバーや参照メンバーがある場合は明示的な初期化が必要なため引数無しのデフォルトコンストラクタは作られません。作ることができないからです。
デフォルトでコピーコンストラクタも作られます。クラス名をMyHistogram
とするとMyHistogram( MyHistogram & )
という形のコンストラクタが作られ、メンバーのコピーがされます。
MyHistogram h1( 100 ); MyHistogram h2 = h1;
この例ではh2
のオブジェクトが作られるときデフォルトコピーコンストラクタが使われます。(明示的にMyHistogram
のコピーコンストラクタが定義されていなければ) ここでちょっと注意が必要です。MyHistogram
クラスはm_store
というポインターメンバーを持っています。h2
のm_store
もh1
からコピーされて同じ値をポインターとして持つことになります。これははなはだ都合が悪いことです。h2
にフィルすると同時にh1
の値も変わります。また、h1
がdelete
されたときm_store
もなくなるのですが、h2
はそのなくなったメモリー領域を指し続けることになります。このようにポインターメンバーを持っている場合正しい動作をするようにコピーコンストラクタを用意してやるか、決してデフォルトコピーコンストラクタが呼び出されないようにプログラムを書く必要があります。
コンストラクタとは関係ありませんが、同じクラスに属すオブジェクト間で代入演算が暗黙に定義されています。実行文として
h2 = h1;
が可能です。この場合も同様な注意が必要です。メンバーオペレータ=
をちゃんと用意しなければなりません。メンバーオペレータの中で元のm_store
に上書きして新しい記憶域を割り当てると以前の領域が管理から離れてどうしようもなくなってしまいます。メモリーリークの例です。
さらに、左辺と右辺が同じ場合も考慮しなければなりません。
h1 = h1;
この場合、何もしてはいけません。以上を考慮すると、オペレータ=の実装は次のようになるでしょう。
MyHistogram & MyHistogram::operator = ( MyHistogram & h ) { if( this == & h ) return * this; if( m_store ) delete [] m_store; m_bincount = h.m_bincount; m_store = new doulbe[ m_bincount ]; return * this; }
this
というのが出てきました。
メンバー関数の定義の中でこれまで特に指定無しにメンバーデータやメンバー関数をそのまま使っていました。しかし通常メンバー関数が呼ばれるのは(静的メンバー関数を除き)クラスのオブジェクトが生成され、そのオブジェクトに付属するものとしてメンバー関数が呼ばれています。オブジェクト自分自身を表す特別のポインターが用意されています。this
ポインターと呼ばれます。上の例で
m_bincount = nbins; m_store = new double[ nbins ];
と書いたのは実は
this -> m_bincount = nbins; this -> m_store = new double[ nbins ];
と書くことと同じです。通常は必要ありませんが、例えば自分自身のオブジェクトへの参照を返さなければいけないとき、例えば
MyHistogram & MyHistogram::func( ) { ... return * this; }
というような書き方をします。this
がなければこのような表現は出来ません。
ここでスコープについてまとめておきましょう。
スコープとは、そのオブジェクトがどの範囲まで見えるかを表したものです。これまでに出てきたものとしては
といったものでした。
万一同じ名前のものがある場合は、より内側の定義のものが採用されます。例えば、
int status_code; //(1)大域変数として定義。 void func( ) { //ある関数の中 int status_code; //(2)関数内の局所変数として定義。 for( int i = 0; i < imax; i ++ ) { //このiはfor文の内部で定義 int status_code; //(3)複文内の局所変数として定義。 status_code = 1; //これは複文内の局所変数(3)が使われる。 } status_code = 2; //これは関数内で定義された(2)が使われる。 }
当然ですが、このように同じ名前を異なる場合に使用することが望ましいはずがありません。ATLASではやってはいけない。
さらにクラスがこれに含まれると
ということになります。局所変数と区別するためにも、メンバー関数をm_
で始めることのメリットは明らかです。
スコープに関して言えば、さらには、namespaceというのも出てきました。これは大域変数同士で名前の衝突が起こるのを避けるために導入されたもので、上のリストで言うならば1と2の間に入るものです。詳しくは後述します。
スコープについて考えていくとオブジェクトライフタイムがイメージできます。例えばfor
ループの中で定義されたオブジェクトはその中で生まれ、for
ループが終わると消滅します。
同様に関数内で定義されたオブジェクトは、定義されたその場所で生成され、関数から制御が離れると消滅します。main
関数も関数ですからmain
関数内で生成されたオブジェクトはmain
関数から制御が離れるときに消滅します。
それに対し大域変数として定義されたオブジェクトはプロセス起動時に生成されプロセス終了時に消滅します。最も長い寿命を持つことになります。プロセスのライフサイクルを超えて生存し続けることはオブジェクトにはできません。
しかしアプリケーションの観点からはプロセスが持っているオブジェクトをいったんファイルに保存したり、別のプロセスに転送したりしたくなります。プロセスのライフより長くオブジェクトが生存し続けることをオブジェクト永続性と呼び、色々な方法が考えられています。残念ながら永続性は言語そのものの仕様には含まれていません。
new
で生成されたオブジェクトは少しやっかいです。delete
されない限りオブジェクト自身は生存し続けます。ところがそれを指し示すポインターはそのオブジェクトとは無関係に生成されており、それ自身の寿命を持ちます。局所変数として作られたポインターは関数から抜けるときに消滅してしまいます。その結果オブジェクトは参照の手段を失い、プロセスが終了するまでメモリーを占拠したままなにもせず居座り続けます。
逆のケースも問題です。オブジェクトはdelete
によっていつ消滅するかわかりません。オブジェクトのポインターがコピーされて使われていると片方のポインターを使ってdelete
しても他のポインターの方がまだ相手のオブジェクトが存在すると思ってアクセスすることもあり得ます。この場合予期せぬことが起こります。
new
で生成されたオブジェクトが必ずdelete
されるようにプログラムを書くのはプログラマーの責任です。心してnew
してください。ATLASでもnewとdeleteの作法を決めています。
Java等の言語ではこういった問題を防ぐ手段を言語処理系が内蔵しています。生成されたオブジェクトには参照カウンターが割り当てられます。同じオブジェクトのアドレスが他のポインターにコピーされると参照カウンターが1増加します。そのポインターの内容が他のアドレスに書き換えられるとカウンターの値は1減らされます。そうやって参照カウンターの値が0になるとオブジェクトは自動的にdeleteされてしまいます。
一番最初のHello! World
の例で、標準出力cout
に文字列を出力するのに<<
という演算子を用いました。元々演算子<<
は整数に作用して左シフトをするものでした。これが標準出力に使えるというのはostream
クラスのオブジェクトcout
を左辺においたときの演算子<<
の多重定義がされているからです。
同じように上述のヒストグラムオブジェクトをcout
に<<
出来るようにしてみましょう。
std::ostream & operator << ( std::ostream & os, MyHistogram & h );
これは大域的オペレータ<<
に左辺値std::ostream
型、右辺値にMyHistogram
型の参照を引数とするものをオーバーロードしています。これは演算子の宣言です。演算子は関数の一種と考えられます。この実装は
std::ostream & operator << ( std::ostream & os, MyHistogram & h ) { h.draw( os ); return os; }
となるでしょう。この例ではostream
オブジェクト(への参照)が左辺項になるために大域演算子のオーバーロードとして実装しました。ostream
クラスの修正で同じことは可能になりますが、誰もそのようなostream
クラスは使ってくれないでしょう。さて、この演算子を使うときは自然に
MyHistogram mh( 100 ); ... //mhを埋めるシーケンスがここにあると思ってください。 std::cout << mh;
オブジェクトが演算の左辺値になれる場合はクラスの中でその演算子を多重定義することも可能です。例えばヒストグラム同士の足し算をします。+=
演算子を多重定義して別のヒストグラムの中身を加算しましょう。
class MyHistogram { public: ... //以前の定義をそのまま。次の行を追加。 MyHistogram & operator += ( MyHistogram & his ); .... };
クラス内で演算子を多重定義する場合、左辺値はそのクラスのオブジェクトそれ自身this
です。この演算子の実装は次のようになるでしょう。
MyHistogram & MyHistogram::operator += ( MyHistogram & his ) { double p1 = m_store; double p2 = his.m_store; //privateなメンバーだけれどもクラス内なので見える。 for( int i = 0; i < m_binsize; i ++ ) { if( i >= his.m_binsize ) break; * p1 ++ += * p2 ++; //ポインターを進めつつ、相手の値を加算している } return * this; //参照を返す必要がある }
これを使うと
MyHistogram h1( 100 ); MyHistogram h2( 100 ); ... h1 += h2;
等という表現が可能になります。同様にoperator *= ( double a )
なども使えますね。ビンの内容をa
倍します。
ここではクラスに関するプログラミングテクニックを少し紹介します。
コンストラクタやデストラクタは自動的に呼ばれます。プログラマーが意識しないうちに呼ばれています。デフォルトコンストラクタは明示的に定義されなければコンパイラが用意をしてしまいます。何かの理由で必ず引数付きのコンストラクタを呼んでほしいときも、利用者はそういうことを知らないので
MyHistogram h;
などと書いてしまいます。この場合、記憶域が確保されないままh
が使われ始めることになります。コンパイラにこのような表現を発見させ禁止させる方法として引数無しコンストラクタを非公開とする方法があります。
class MyHistogram { public: MyHistogram( int nbins ); //使ってほしいコンストラクタ private: MyHistogram( ); //使ってほしくないコンストラクタをprivateにする。 };
もし上述のような表現があるとh
の引数無しコンストラクタ呼び出しをコンパイラが適用し、それへの参照がprivate
で禁止されていることを検出することによって防止することが出来ます。
アプリケーションによっては、クラスのオブジェクトを一つだけ作ってそれがアプリケーションを通してある部分のデータを管理するようにしたいと言うことがあります。二つ以上のオブジェクトを作れないようにし、それがほしい場合は静的メンバー関数でそれを返すようにします。シングルトンと呼ばれます。クラス定義は次のようになります。
class Singleton { public: static Singleton * get( ); private: Singleton( ) { } static Singleton * s_singleton; };
また、メンバーの実装は次のようになります。
Singleton * Singleton::s_singleton; Singleton * Singleton::get( ) { if( ! s_singleton ) s_singleton = new Singleton( ); return s_singleton; }
ユーザがさわれるメソッドはget
だけです。静的な記憶域s_singleton
をポインターとしてオブジェクトが一つだけ作られます。
まずクラスを用意します。ヘッダーファイルはSingleton.h
です。
#ifndef __SINGLETON_H #define __SINGLETON_H class Singleton { public: static Singleton * get( ); private: Singleton( ) { } static Singleton * s_singleton; }; #endif
次にメンバー定義を行います。Singleton.cxx
です。
#include "Singleton.h" Singleton * Singleton::s_singleton = 0; Singleton * Singleton::get( ) { if( ! s_singleton ) s_singleton = new Singleton( ); return s_singleton; }
最後にメイン関数を用意します。
#include <iostream> #include "Singleton.h" main( ) { Singleton * s = Singleton::get( ); }
実行しましょう。
g++ -c Singleton.cxx
g++ -c msingleton.cxx
g++ msingleton.o Singleton.o
./a.out
なにも起こりませんが無事に走りました。次にメインを
main( ) { Singleton s; }
としてやってみてください。
g++ -c msingleton.cxx
msingleton.cxx: In function `int main()':
Singleton.h:8: error: `Singleton::Singleton()' is private
msingleton.cxx:6: error: within this context
次回はクラスの継承について説明します。