ATLAS日本基礎ネットワーク C++トレーニングコース

第四回 クラス

ここでの目標

目次

1.クラスの定義

クラスとはユーザが定義する型です。型とはそれに属するオブジェクトを作る枠のことです。これまでの型は組込型でした。整数とか浮動小数点数とか、C++にもともと用意されているものでした。その型に対する演算も組み込まれていました。

これからはクラスを使ってユーザが自由に型を加えていくことが出来ます。型、すなわちオブジェクトが持つべき形を決めると同時に、オブジェクトと他の型のオブジェクトの間の演算を定義することが出来ます。さらに、メソッドと呼ばれる、オブジェクトに固有の関数を定義し、プログラムの中でのオブジェクトの振る舞い方を規定することが出来ます。プログラムは、モデルとしては、いくつかの型のオブジェクトが相互作用しながらタスクを実現していくことになります。

1.1.クラス定義

クラスはメンバーデータとメンバー関数によって構成されます。クラスの定義では、そのクラスにはどのようなメンバーデータとメンバー関数があり、それぞれどのように使えばよいかを記述します。クラス定義は例えば次のような形になります。

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_で始まる名前を持つことになっています。

1.2.クラス定義ファイル

実際にクラスを定義するファイルを作りましょう。このクラス定義ファイルは2つの用途があります。一つはクラスを供給する側がメンバー関数やメンバーデータを定義するために必要です。もう一つはこのクラスの利用者がこのクラスのオブジェクトにアクセスするためにインターフェースとして必要になります。そのため、メンバー関数の定義ファイルなどとは分けて、ヘッダーファイルとして用意します。

実習1.

今日の作業場所を用意します。

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でマクロを定義する唯一のケースです。

2.クラスの使用・オブジェクト

クラスはあくまでも型です。整数型のオブジェクトを定義して変数として使うように、プログラム中ではそのクラスのオブジェクトを定義してそれを使います。

2.1.クラスのオブジェクト

クラスのオブジェクトを作るためには、その段階までにクラスが定義されていなければなりません。クラス定義は、通常そのクラス名に.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の後に.(ピリオド:メンバー選択演算子)を置いて、それに続けてメンバー関数を指定します。

実習2.

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に書いてあるからここでは書かないというのはプログラムを理解しにくくします。

2.2.オブジェクトのnewによる生成

もう一つのオブジェクトの利用の仕方を見ましょう。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という演算子が用意されていると言うことはこれを使わないと実現できない機能があるからなのです。これについては次回説明します。

実習3.

やってみましょう。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

ここまで二つのクラス利用例を見てきましたがクラスメンバーの実装そのものはまだ行われていないことに注意してください。クラスの利用者はそのクラスのヘッダーさえ用意されればそれでプログラム開発ができます。クラスヘッダーはそのクラスの仕様書の役割をします。クラスの実装がどのように変わってもそのクラスの仕様書(クラスヘッダー)が変わらなければ利用者側はプログラムを書き換える必要がありません。開発者と利用者の隔離がクラス導入の重要なメリットの一つです。

3.メンバーデータ

クラスはその構成物としてデータと関数を持ち、それぞれ、メンバーデータ、メンバー関数(メソッド)と呼ばれます。C言語で実装された構造体structは、メンバーデータだけを持ったクラスと考えることが出来ます。この場合、データにアクセスする方法(メソッド)が用意されていないので直接メンバーデータに読み書きを施しました。クラスの場合、メソッドが用意されているので、クラスの利用者が直接メンバーデータをアクセスしなくても良くなりました。このことは画期的なことでした。つまり、

ということで、仕様と実装を分離することが可能になり、より大規模なプログラム開発が効率的に行えるようになりました。この仕組みを実装のカプセル化と呼びます。

ここでわかるように、利用者には一部のメンバー関数を公開し、メンバーデータやその他のメンバー関数を非公開にすることが期待されます。先の例でもpublicなのはメンバー関数だけであり、メンバーデータはprivateに置かれました。

3.1.メンバーデータの定義

メンバーデータには任意の型のオブジェクトを定義することが可能です。これまで見てきたような組込型(longdoubleなど)はもちろん、他のクラスのオブジェクトをメンバーデータとして持つこと、オブジェクトへのポインターを持つことももちろん可能です。

ATLASではメンバーデータの名前はm_で始まらなければなりません。また、すべてのメンバーデータはpublicにしてはいけません。

先ほどの例では

private:
    int m_bincount;
    double * m_store; 

の部分です。

3.2.静的な(static)メンバーデータ

静的なメンバーデータを定義することも出来ます。静的なメンバーデータはクラスのオブジェクトの生成・破棄と無関係に、最初から最後まで存在します。そして、その間、データを保持し続けます。また、そのクラスのすべてのオブジェクトの間で値を共有することになります。

class StaticMemberClass {
private:
    static int s_state;
};

この静的なオブジェクトのために記憶域を定義しておく必要があります。

int StaticMemberClass::s_state = 0;

これは通常メンバー関数を記述するファイルに実装としておかれます。後で述べますが、StaticMemberClass::StaticMemberClassというクラスに属するという意味です。

ATLASでは、静的なメンバーの名前s_で始まらなければなりません。

4.メンバー関数

メンバー関数はクラスの振る舞いを規定します。上で説明したように、利用者は公開されたメソッドすなわちメンバー関数を見て、そのクラスの役割を理解します。

4.1.メンバー関数の定義

クラス定義がクラス名.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++では次に述べるコンストラクタとデストラクタ以外は文法的にメンバー関数を分類する仕組みはありませんが、機能的には分類可能です。クラスの設計を寄り合理的にするためにメンバー関数のカテゴリー分けをすることは有効です。

4.2.コンストラクタとデストラクタ

通常のメンバー関数は、それを利用するものが呼び出すことで初めて制御がそこへ移ります。C++のクラスではプログラマーが指定しないにもかかわらず暗黙に呼び出される二つのメンバー関数があります。クラスのオブジェクトのライフタイムを管理する上で必要なものなので、クラスの開発者がそれを記述しないと、コンパイラが自動的にデフォルトの関数を採用します。その二つのメンバー関数とはコンストラクタ(構築子)とデストラクタ(破棄子)です。

コンストラクタはそのクラス名と同じ名前の関数として記述されます。一方、デストラクタは~を前につけ、続けてクラス名と同じ名前をおいた関数として記述されます。先の例ではコンストラクタがMyHistogram()、デストラクタは~MyHistogram()になります。コンストラクタもデストラクタも戻り値を持ちません。

コンストラクタは引数を持つことが出来ますが、デストラクタは出来ません。関数オーバーロードの仕組みにより、コンストラクタはいくつも実装できますが、引数をもてないデストラクタは一つしか実装できないことになります。

あるクラスのオブジェクトに関して、生成時にコンストラクタが呼び出され、破棄時にデストラクタが呼び出されると言うことは、コンストラクタの中で動的にメンバーオブジェクトが割り付けられ、デストラクタの中でそれが解放されることを想定しているとも考えられます。

MyHistogram::MyHistogram( int nbins ) {
    m_bincount = nbins;
    m_store = new double[ nbins ];
}

m_storedouble型の記憶域が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のコピーコンストラクタを呼び出しています。この初期化は{}に制御が渡される前に行われるため、{}では既に初期化済みの変数として使用することが出来ます。

実習4.

ここまでで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.out
   0:      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

ヒストグラムの実装についてはもっともっと気の利いたものがありそうですね。

4.3.デフォルトコンストラクタ

ある条件が整うとクラスのコンストラクタが自動的に作られる場合があります。それはデフォルトコンストラクタと呼ばれます。

あるクラスのコンストラクタが全く明示的には定義されていない場合、引数無しのコンストラクタが自動で生成される場合があります。そのコンストラクタではメンバーデータの型に応じそれぞれ引数無しのコンストラクタを呼び出します。

メンバーデータにconstなメンバーや参照メンバーがある場合は明示的な初期化が必要なため引数無しのデフォルトコンストラクタは作られません。作ることができないからです。

デフォルトでコピーコンストラクタも作られます。クラス名をMyHistogramとするとMyHistogram( MyHistogram & )という形のコンストラクタが作られ、メンバーのコピーがされます。

MyHistogram h1( 100 );
MyHistogram h2 = h1;

この例ではh2のオブジェクトが作られるときデフォルトコピーコンストラクタが使われます。(明示的にMyHistogramのコピーコンストラクタが定義されていなければ) ここでちょっと注意が必要です。MyHistogramクラスはm_storeというポインターメンバーを持っています。h2m_storeh1からコピーされて同じ値をポインターとして持つことになります。これははなはだ都合が悪いことです。h2にフィルすると同時にh1の値も変わります。また、h1deleteされたときm_storeもなくなるのですが、h2はそのなくなったメモリー領域を指し続けることになります。このようにポインターメンバーを持っている場合正しい動作をするようにコピーコンストラクタを用意してやるか、決してデフォルトコピーコンストラクタが呼び出されないようにプログラムを書く必要があります。

4.4.オブジェクトのコピー

コンストラクタとは関係ありませんが、同じクラスに属すオブジェクト間で代入演算が暗黙に定義されています。実行文として

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というのが出てきました。

4.5.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がなければこのような表現は出来ません。

5.スコープ〜名前の解決

ここでスコープについてまとめておきましょう。

5.1.スコープ

スコープとは、そのオブジェクトがどの範囲まで見えるかを表したものです。これまでに出てきたものとしては

  1. 大域変数。プログラム上のいかなる場所からも見える変数。
  2. 局所変数。関数の中でのみ見える。
  3. 複文の中で定義された局所変数はその複文の中でのみ見える。

といったものでした。

万一同じ名前のものがある場合は、より内側の定義のものが採用されます。例えば、

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ではやってはいけない。

さらにクラスがこれに含まれると

  1. 大域変数。プログラム上のいかなる場所からも見える変数。
  2. クラスメンバーデータ。メンバー関数の定義の中から見える。
  3. 局所変数。関数の中でのみ見える。
  4. 複文の中で定義された局所変数はその複文の中でのみ見える。

ということになります。局所変数と区別するためにも、メンバー関数をm_で始めることのメリットは明らかです。

スコープに関して言えば、さらには、namespaceというのも出てきました。これは大域変数同士で名前の衝突が起こるのを避けるために導入されたもので、上のリストで言うならば1と2の間に入るものです。詳しくは後述します。

 

5.2.オブジェクトのライフタイム

スコープについて考えていくとオブジェクトライフタイムがイメージできます。例えばforループの中で定義されたオブジェクトはその中で生まれ、forループが終わると消滅します。

同様に関数内で定義されたオブジェクトは、定義されたその場所で生成され、関数から制御が離れると消滅します。main関数も関数ですからmain関数内で生成されたオブジェクトはmain関数から制御が離れるときに消滅します。

それに対し大域変数として定義されたオブジェクトはプロセス起動時に生成されプロセス終了時に消滅します。最も長い寿命を持つことになります。プロセスのライフサイクルを超えて生存し続けることはオブジェクトにはできません。

しかしアプリケーションの観点からはプロセスが持っているオブジェクトをいったんファイルに保存したり、別のプロセスに転送したりしたくなります。プロセスのライフより長くオブジェクトが生存し続けることをオブジェクト永続性と呼び、色々な方法が考えられています。残念ながら永続性は言語そのものの仕様には含まれていません。

newで生成されたオブジェクトは少しやっかいです。deleteされない限りオブジェクト自身は生存し続けます。ところがそれを指し示すポインターはそのオブジェクトとは無関係に生成されており、それ自身の寿命を持ちます。局所変数として作られたポインターは関数から抜けるときに消滅してしまいます。その結果オブジェクトは参照の手段を失い、プロセスが終了するまでメモリーを占拠したままなにもせず居座り続けます。

逆のケースも問題です。オブジェクトはdeleteによっていつ消滅するかわかりません。オブジェクトのポインターがコピーされて使われていると片方のポインターを使ってdeleteしても他のポインターの方がまだ相手のオブジェクトが存在すると思ってアクセスすることもあり得ます。この場合予期せぬことが起こります。

newで生成されたオブジェクトが必ずdeleteされるようにプログラムを書くのはプログラマーの責任です。心してnewしてください。ATLASでもnewとdeleteの作法を決めています。

Java等の言語ではこういった問題を防ぐ手段を言語処理系が内蔵しています。生成されたオブジェクトには参照カウンターが割り当てられます。同じオブジェクトのアドレスが他のポインターにコピーされると参照カウンターが1増加します。そのポインターの内容が他のアドレスに書き換えられるとカウンターの値は1減らされます。そうやって参照カウンターの値が0になるとオブジェクトは自動的にdeleteされてしまいます。

6.演算子の多重定義

一番最初のHello! Worldの例で、標準出力coutに文字列を出力するのに<<という演算子を用いました。元々演算子<<は整数に作用して左シフトをするものでした。これが標準出力に使えるというのはostreamクラスのオブジェクトcoutを左辺においたときの演算子<<の多重定義がされているからです。

6.1.大域的オペレータのオーバーロード

同じように上述のヒストグラムオブジェクトを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;

6.2.メンバーオペレータ

オブジェクトが演算の左辺値になれる場合はクラスの中でその演算子を多重定義することも可能です。例えばヒストグラム同士の足し算をします。+=演算子を多重定義して別のヒストグラムの中身を加算しましょう。

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倍します。

7.クラスに関するテクニック

ここではクラスに関するプログラミングテクニックを少し紹介します。

7.1.privateなコンストラクタ

コンストラクタやデストラクタは自動的に呼ばれます。プログラマーが意識しないうちに呼ばれています。デフォルトコンストラクタは明示的に定義されなければコンパイラが用意をしてしまいます。何かの理由で必ず引数付きのコンストラクタを呼んでほしいときも、利用者はそういうことを知らないので

MyHistogram h;

などと書いてしまいます。この場合、記憶域が確保されないままhが使われ始めることになります。コンパイラにこのような表現を発見させ禁止させる方法として引数無しコンストラクタを非公開とする方法があります。

class MyHistogram {
public:
    MyHistogram( int nbins );    //使ってほしいコンストラクタ
private:
    MyHistogram( );        //使ってほしくないコンストラクタをprivateにする。
};

もし上述のような表現があるとhの引数無しコンストラクタ呼び出しをコンパイラが適用し、それへの参照がprivateで禁止されていることを検出することによって防止することが出来ます。

7.2.シングルトン

アプリケーションによっては、クラスのオブジェクトを一つだけ作ってそれがアプリケーションを通してある部分のデータを管理するようにしたいと言うことがあります。二つ以上のオブジェクトを作れないようにし、それがほしい場合は静的メンバー関数でそれを返すようにします。シングルトンと呼ばれます。クラス定義は次のようになります。

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

次回はクラスの継承について説明します。


前へ 上へ 次へ


2017年7月21日