C++入門/Class内部の隠蔽・メンバとコンストラクタ

柴田祐樹,東京都立大学,情報科学科
戻る

Classと型では,新しい型を作り,演算を割り当て,数値型よりも抽象化の進んだ対象(オブジェクト)をC++で扱う方法を説明した.ここでは,オブジェクトの内部と外部を分離し,この抽象化を推し進めて,コードの可読性を向上させ,どのように再利用可能であるかを明確にする.具体的には,プログラムをうごかすために必要な変数と実行コード制御文を一つのクラスとしてまとめて,別々に扱わずに済む方法をこれから学ぶ.ここでは具体的に以下の概念を説明する.

後半の具体的な題材には,SVG (Scalable Vector Graphics) を用いる.SVGに不慣れなものは先にsvg.htmlを見られよ.

メンバ変数とメンバ関数

C++のクラスの機能である変数の隠蔽について説明する.これはオブジェクト(クラスで与えられる変数の実態)の内部状態,すなわちメンバ変数の値は「メンバ関数」を通してのみ変更を認める,という設計方針である.組織に営業や受付がいて通常組織内部の開発者と直接発注受注を行わないのと同じ構造である.かならず窓口を通して内部へ影響を及ぼすことで,不必要な内部の状態をクラスの利用者が知らずにすみ,クラスを用いたコード作成における負担が減る.コードの見通しが良くなる,などの利点がある設計方針である.以下のコードを用いて具体的に説明していく.

#include <iostream>
using namespace std;
struct A{
    double x;
    void inc(){
        x = x + 1;
    }
};

int main(){
    A a, b;
    a.x = 0; b.x = 0;
    a.inc();
    a.inc();
    b.inc();
    cout << a.x << ", " << b.x << "\n";
    return 0;
}
            
メンバ変数とメンバ関数

クラスAが3-8行で定義されている.これが,main関数の中で使われている.Aはメンバ変数xとメンバ関数incを持つ.クラスの定義ブロック内部に定義された関数は,そのクラスのメンバ関数と呼ぶ.つまりincは今Aのメンバ関数である.メンバ関数はメンバ変数にアクセスできるのだが,実際にアクセスするメンバ変数の実態(メモリの場所)は,クラスにより生成されたオブジェクト事に異なる.具体的に,12行目から15行目の動作にこのことが現れるので,12-15行について見ていく.

このコードでは,クラスAのオブジェクトa, bが生成される.a.xとb.xは,クラスAのメンバ変数としては同じxであるが,それぞれ異なるオブジェクトa, bに属する2つのxが存在する.名前が同じで親が違う二人の子供,ということである.この時,a.inc(), b.inc()のように,メンバ関数を参照演算子.で呼び出すと,incの中で対象とされるxは,a.inc()のときはa.xとなり,b.inc()のときはb.xとなるのである.実際にこのコードを実行すると,16行目の出力からa.x = 2, b.x = 1とわかるため,今説明した動作の通りになっていることがわかるだろう.C++のクラスを使いこなすためには,この動作を行うことを念頭に,メンバ関数,メンバ変数,オブジェクトの関係を理解しなければならない.

コンストラクタ

先の例では,12行目のようにメンバ変数の初期化をメンバ関数を通さずに行っていた.これは好ましい記載方法ではない.メンバ変数の操作方法をクラスの利用者が気にしなくて済むようにしコード作成上の利便性を最大化しバグの発生機会を最小化するためには,すべてのメンバ変数の操作は,メンバ関数により行うべきであるため,この初期化操作をメンバ関数により行うように修正する.この時「オブジェクトは生成された時点で初期化が終わり使える状態と鳴っていなければならない」という指針があるのだが,メンバ関数を呼び出すことはオブジェクトを生成した後でなければできないので,この指針に従うためにはすこし特別な関数が必要である.そこで,コンストラクタと呼ばれれう関数が用意されている.これを説明する.

にコンストラクタを用いた例を示す.5-7行目に示すのがコンストラクタである.

#include <iostream>
using namespace std;
struct A{
    double x;
    A(){
        x = 0;
    }
    A(double x_){
        x = x_;
    }
    void inc(){
        x = x + 1;
    }
};

int main(){
    A a, b;
    A c(2);
    b = A(3);
    b = A();
    a.inc();
    a.inc();
    b.inc();
    cout << a.x << ", " << b.x << ", " << c.x << "\n";
    return 0;
}
コンストラクタの使用例

コンストラクタの関数名は,A()と書かれている通り,クラス名と同じにする.戻り値は定義しない.この用に定義した関数が存在すると,オブジェクト生成時に,この関数が呼び出されるようにコンパイルされるようになる.実際に,14行目で,a, b を宣言するわけだが,この場所で,a.A()などのように呼び出さずとも,A()がそれぞれa, bに対し実行されるのである.これにより,いかなる他のメンバ関数を呼び出される前に,確実にメンバ変数を初期化することが可能となる.

8-10行目のAの定義は,コンストラクタのオーバーロードという記法である.5-7行目と比べればわかるように,引数があるかどうかの違いがある.このように,引数が違うコンストラクタを複数定義して置くと,引数を与えてクラスAが使われた場合と引数なしで使われた場合に,初期化の動作を変えることができる.例えば,18行目では引数を与えているため8行目の関数Aが呼び出され,17行目では引数を与えていないため5行目の関数Aが呼び出される.引数の与え方は,18行目のように変数名に関数呼び出し演算子「( )」をつけて与える方法と,19行目のように,クラス名に引数を与えて呼び出す方法がある.19行目は,すでにある変数を再度コンストラクタで初期化したい場合に使うことができる.20行目は引数のないコンストラクタによる再初期化の例である.この = の右側にコンストラクタがある場合は,実はここに一時的なオブジェクトが生成されて,=の左側にそれが代入される,とみなすこともでき,そのように解釈するとコード作成の自由度が向上する場合があり,これについて次の節で,これに関して応用上価値のあるベクトルにより紹介する.

ちなみに引数のないコンストラクタを「デフォルトコンストラクタ, Default constructor」と呼ぶ.

ベクトルクラスへのコンストラクタの導入

ベクトルクラスにコンストラクタを導入する例を,クラスと型のページで紹介していた以下のクラスR2のコードを改良することで示す.このコードのgenR2という関数をコンストラクタで定義し直そう,ということである.自ら考えてみてほしい.

#include <iostream>
struct R2{
    double x;
    double y;
};
R2 genR2(double x, double y){
    R2 r;
    r.x = x;
    r.y = y;
    return r;
}
R2 operator+(R2 a, R2 b){
    R2 r;
    r.x = a.x + b.x;
    r.y = a.y + b.y;
    return r;
}
int main(){

    R2 a, b, c;

    a = genR2(1, 2.5);
    b = genR2(1, 1.5);
    c = a + b;

    std::cout << c.x << ", " << c.y << "\n"; // 2, 4 と表示されるはず.
    return 0;
}

答えの例をvecexans.cppに示す.

SVGによるグラフ描画クラスの作成課題

SVGを保持するオブジェクトに必要な関数は,対応するSVGの範囲により異なる.完全なSVGのオブジェクトを作ろうとしたら,XML(Xtensible Markup Language)に対応しなければならないが,ここではそこまでは扱わない.点や線により数学のグラフをSVGで描く程度のことを目指す.このため,まずは,文字列の情報を保持し,その文字列を改変していくこととする.以下に,基本となるSVGによりグラフを描画するクラス SVGGraph を示す.

#include <iostream>
#include <fstream>
#include <string>
using namespace std;
struct SVGGraph{
    double vx0, vy0, H, W;
    string s;
    SVGGraph(){
        vx0 = -1.2; vy0 = -1.2; W= 2.4; H= 2.4;
        s = "";
    }
    void circle(){
        s += "<circle r='0.2' cx='0.1' cy='0.1' fill='black' />\n";
    }
    string serialize(){
        string r = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='"+to_string(vx0)+" " + to_string(vy0) + " " + to_string(W) + " " + to_string(H) + "'>\n";
        r = r + s + "</svg>\n";
        return r;
    }
};
int main(){
    SVGGraph o;
    o.circle();
    ofstream of("testGraph.svg");
    of << o.serialize();
    return 0;
}
SVGGraph によるグラフの描画

これを実行すると,testSVG.svgなるファイルが出力される.これは,ブラウザ等SVGを可視化するソフトで開けばのように見えることだろう.

Code 例1の出力結果の例

このコードに対して,以下の改変を加えてみよう.

SVGのtext要素とline要素の使い方は次のとおりである.文字と線分をそれぞれ描画することができる.

<svg viewBox="-1 -1 2 2"  xmlns="http://www.w3.org/2000/svg">
<text style="font-size:0.2;"  transform="translate(0.5, 0.5) rotate(-90)">test a text</text>
<text style="font-size:0.2;"  transform="translate(-0.5, 0.5) rotate(0)">test a text 2</text>
<line x1="-0.8" y1="-0.8" x2="0.8" y2="-0.8" style="stroke:rgb(0,0,0);stroke-width:0.02" />
<line x1="-0.8" y1="-0.8" x2="-0.8" y2="0.8" style="stroke:rgb(0,0,0);stroke-width:0.02" />
</svg>

これを表示するとのようになる.

text の表示例

拡張子グラフを描画するための参考のコードをgraphclass_ans.cppに示す.また,このコードにより生成される画像は,のようなものである.Leap frog法により円軌道の微分方程式を解くことで円周上の点を生成し,作成したクラスのオブジェクトで描画している.

graphクラスを拡張した結果の例