第3回 C++・クラスを使ったプログラミング
(データ保護・コンストラクタなど)


vectorクラスあれこれ

C++のSTLであるvectorクラスは大変便利です。 そこで、vectorクラスで利用できる関数や操作方法の代表的なものをまとめておきます。 なお、対応するJava, Pythonでの類似データ構造と比較しておきます。 vector <T> data;が宣言されていると仮定します。 JavaではArrayList <T> data;が宣言され、Pythonでは、 listとしてdataという変数名を使うと仮定します。

関数 vector (C++) ArrayList (Java) list (Python) numpy array (Python)
宣言 #include <vector> import java.util.ArrayList; なし import numpy as np
初期化 vector <T> data; ArrayList <T> data; data = [] data = np.zeros(0)
100個要素確保 vector <T> data(100); ArrayList <T> data =
new ArrayList<T>(100);
data = [None]*100 data = np.zeros(100)
i-番目の要素 data[i] (data.at(i)) get(i) data[i] data[i]
サイズ data.size() data.size() len(data) data.size
データx追加 data.push_back(x) add(x) data.append(x) np.append(data,x)
データクリア data.clear() data.clear() data.clear() N/A
イタレータ(1) for (int i=0;i < (int)data.size();i++) for (int i=0;i < data.size();i++) for i in range(len(data)): for i in range(len(data)):
イタレータ(2) for (auto it=data.begin();
it != data.end(); it++)
  Iterator it=data.iterator();
    while(it.hasNext())
for it in data: N/A
スライシング(from,to)   vector <T>
  subset(data.begin()+from,
              data.begin()+to)
data.sublist(from,to) data[from:to] data[from:to]
連結(dataとdata2) data.insert(data.end(),
data2.begin(),data2.end());
data.addAll(data2) data+data2 np.concatenate((data,data2))
コピー vector <T>data2 = data; ArrayList <T>data2 = new
ArrayList <T>(data)
data2 = data[:] data2 = np.copy(data)
多次元化(m,n) vector <vector <T>data(m,vector<T>(n)); ArrayList < ArrayList <T>>data = new
ArrayList<ArrayList<T>>();
[[0]*n for i in range(m)] np.zeros((m,n))


データ保護

クラスのひとつの見方として、『データ構造とアルゴリズム』を具現化するためのひとつであることを論じました。 この例として線形リストとそれによるスタック構造の操作関数、 あるいはハッシュ関数などは、すでに実装の一例を紹介しました。 今回は、まず、 2分木構造(2分探索木)をクラスで表すことを考えましょう。そこでまず、 以下のような構造体(一部C++11で導入されたusingによるエイリアス宣言を含む)を考えます。


using DATA = char;

struct node {
	DATA d;
	struct node *left;/* 左部分木 */
	struct node *right;/* 右部分木 */
}; 

これをクラスで表すことを考えます。 まず、データを文字型から、より実用的な型に変更します。 次にstructをclassに変更します。 2分木に加えるデータとして食品と以下の表のような100gあたりのGI値、 炭水化物、カロリー、脂質からなるデータを有し、 各クラスのインスタンスで1行分のデータを持たせるとします。

食べ物 GI値 炭水化物 カロリー 脂質 タンパク質
白米 81 37.1 168 0.32.53
食パン 91 46.7 264 4.49.33
そば 54 26.0 132 1.04.8
うどん 85 21.6 105 0.42.6
もち 80 50.3 235 0.84.2
アボガド 27 6.2 187 18.72.5
トマト 30 4.7 19 0.10.7
ショートケーキ 80 47.1 344 14.07.4
バナナ 55 22.5 86 0.21.11
牛乳 25 4.8 67 3.83.3
アイスクリーム 65 23.2 180 8.03.9
プリン 52 14.7 126 5.05.5
チョコレート 91 55.4 557 34.07.4
フライドポテト 85 32.4 388 27.42.9


これより、


struct Food {
	string name; /* 食べ物の名前 */
	float GI; /* GI値 */
	float carbon; /* 炭水化物 */
	float calorie; /* カロリー */
	float fat; /* 脂質 */
	float protein; /* タンパク質 */
};

とします。 C++では structもクラスになりますが、structで定義されるクラスでは、 アクセス修飾子をつけないと、 すべてpublicとみなされます。 繰り返しますが、この structではじまるFood構造体は、C++ではクラスとなります。 データの保護という観点からは、この例では、 すべての変数がpublic相当ですので、「保護せず、すべて公開する」例となっています。 仮に、struct部分をclassという予約語に置き換えると、すべての変数は暗黙的にprivate変数になりますので、その場合は「データ保護がなされ、変数は未公開」となります。 これを以下のようなFood.hという名前のヘッダファイルで作るとします。


Food.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
/* プログラム3-1 食べ物クラス */
#ifndef _FOOD
#define _FOOD
#include <iostream>
using namespace std;
struct Food {
	string name; /* 食べ物の名前 */
	float GI; /* GI値 */
	float carbon; /* 炭水化物 */
	float calorie; /* カロリー */
	float fat; /* 脂質 */
	float protein; /* タンパク質 */
	Food(){}
	Food(string name, float GI, float carbon,
	float calorie, float fat, float protein ): name(name), GI(GI),
	carbon(carbon), calorie(calorie), fat(fat), protein(protein) {}
};
#endif

次に、2分木(2分探索木)をクラスで定義します。 探索できるために、2分木のノードにキーの存在が必要です。 ここでは、Foodクラスに含まれるカロリー値をキーとします。 木はデータの出現順にルートから作成し、 キー値より小さいか等しい場合は左部分木に加え、 大きい場合は右部分木に加えるとします。

BinaryTree.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* プログラム3-2 2分木クラス */
#ifndef _BINARY_TREE
#define _BINARY_TREE
#include <iostream>
using namespace std;

#include "Food.h"

class BinaryTree {
	private:
		Food data;/* キーを含むデータ */
		BinaryTree *left;/* 左部分木 */
		BinaryTree *right;/* 右部分木 */
		void print();/* 外からは呼ばないのでprivate */
	public:
		BinaryTree(){}
		BinaryTree(Food data);/* コンストラクタ */
		BinaryTree(const BinaryTree &tree);/* コピーコンストラクタ */
		~BinaryTree(){}/* デストラクタ */
		void printPreOrder(BinaryTree *);/* 前順序でプリント */
		void printInOrder(BinaryTree *);/* 中順序でプリント */
		void printPostOrder(BinaryTree *);/* 後順序でプリント */
		BinaryTree* insert(BinaryTree *, Food data);/* 木にデータを挿入 */
		void deleteTree(BinaryTree*);/* 木を削除 */
}; 
#endif


この2分木はFoodの中のカロリー値をキーとして左部分木と右部分木を区別します。 内部だけで使う関数print()はprivateで定義しており、 外部(他のクラスやmain関数など)からはアクセスできないようにしています。

BinaryTree.hをヘッダーファイルとして作成したら、そのメンバー関数の実装は、その内容が小さい場合を除いて、 以下のプログラム のようにヘッダーファイルとは別に作成し、 C++言語の拡張子(ここでは.cppファイル)として、作成・保守することがC++言語では通例です。


BinaryTree.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/* プログラム3-3 2分木クラスのメンバー関数定義 */
#include <iostream>
using namespace std;

#include "BinaryTree.h"

/* コンストラクタ */
BinaryTree:: BinaryTree (Food data){
	this->data = data;
	this->left = this->right = nullptr;
}

/* コピーコンストラクタ */
BinaryTree:: BinaryTree (const BinaryTree &tree){
	this->data = tree.data;
	this->left = tree.left;
	this->right = tree.right;
	cout << "コピー コンストラクタが呼ばれました" << endl;
}

/* insertメソッド (再帰的に挿入)*/
BinaryTree *
BinaryTree:: insert(BinaryTree *p, Food data){
	if (p == nullptr) { /* ノードがnullptrなら木のノードデータを生成 */
		return ( new BinaryTree(data) );
	}
	else if (data.calorie <= p->data.calorie) /* キーがノードのキーより小さいか等しいとき左部分木へ挿入 */
		p->left = insert(p->left, data);
	else				/*  そうでないとき右部分木へ挿入  */
		p->right = insert(p->right, data);
	return(p);
}

/* プリントメソッド */
void
BinaryTree:: print(void){
	cout << data.name << "(" << data.calorie << ") "  << endl;
}

/* 前順序でプリント */
void 
BinaryTree:: printPreOrder(BinaryTree *p){
	if (p != nullptr) {
		p->print();
		printPreOrder(p->left);
		printPreOrder(p->right);
	}
}

/* 中順序でプリント */
void 
BinaryTree:: printInOrder(BinaryTree *p){
	if (p != nullptr) {
		printInOrder(p->left);
		p->print();
		printInOrder(p->right);
	}
}

/* 後順序でプリント */
void 
BinaryTree:: printPostOrder(BinaryTree *p){
	if (p != nullptr) {
		printPostOrder(p->left);
		printPostOrder(p->right);
		p->print();
	}
}

/* 2分探索木の再帰的な削除 (後順序) */
void 
BinaryTree:: deleteTree(BinaryTree *p){
	if (p != nullptr){
		deleteTree(p->left);
		deleteTree(p->right);
		delete p;
	}
}

まず、クラス内(ヘッダーファイル)で定義された関数を、このプログラムが定義していることに着目してください。 その際、型のないコンストラクタ以外の関数は、戻り値の型をTとするとき、

T クラス名:: 関数名(引数1, 引数2, ...){ ...}

という形式となっている点がユニークな点かと思います。 具体的な例としては、BinaryTreeへのポインタを返す22~23行目のinsert関数

BinaryTree * BinaryTree:: insert(BinaryTree *p, Food data){ ...}

やvoid型のprintInOrder関数(51~52行目)の例では

void BinaryTree:: printInOrder(BinaryTree *p){ ...}

のようになります。

再帰的な関数である21-32行目のinsert関数についてコメントします。 特に

p->left = insert(p->left, data);

の意味は、左部分木であるp->left以降のデータをinsert()関数で結果を左部分木に再帰的に挿入することを表します。 引数に左辺と同じパラメータが登場している点に注意してください。 意味は、左部分木(のどこかのkeyであるdata.calorieの位置)に、 dataを再帰的に挿入しています。

2分木クラス(BinaryTreeクラス)には、 木にノード(ここではFoodクラスのデータ)を挿入する関数、前順序でプリントする関数、 中順序でプリントする関数、後順序でプリントする関数、木を削除する関数など、様々用意しています。 これらの関数では、BinaryTreeクラスのルートノードからポインタを再帰的にたどる手法を共通に利用している点にも注意してください。 上の2つのヘッダーファイルをもとに、 main関数でデータを読み込み、2分木を作成する部分のサンプルコード例を以下に示します。


FoodTree.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/* プログラム3-4 食べ物クラスのカロリーをキー
とする2分木を作成し、中順序でプリントする */
#include "BinaryTree.h"
#include <fstream>
#include <sstream>
#include <cstdlib>
#include <vector>

vector <string> split(string& input, char delimiter)
{
    istringstream stream(input);/* 入力文字列をistringstreamとして処理 */
    string field;
    vector <string> result;/* 結果の文字列リストを保持 */
    while (getline(stream, field, delimiter)) {
        result.push_back(field);/* データをvectorにpush */
    }
    return result;
}

int main(int ac, char **av){
    if (ac != 2){
        cout << "FoodTree ファイル名[食べ物](CSV)" << endl;
        return 1;
    }
    string fname = av[1];/* ファイル名をコマンドラインの第一引数で与える */
    ifstream fin(fname.c_str(), ios::in);/* ファイルをオープン */
    if (fin.fail()){/* ファイルオープンできないとき */
        cerr << "ファイル:" << fname << "がオープンできませんでした" << endl;
        exit(EXIT_FAILURE);/* 終了:#include <cstdlib>を必要とする */
    }
    string name;/* 食べ物の名前 */
    float GI;/* GI値 */
    float carbon;/* 炭水化物 */
    float calorie;/* カロリー */
    float fat;/* 脂質 */
    float protein;/* タンパク質 */
    string line;/* 読み込んだ行をセーブ */
    getline(fin, line); /* 最初の行をスキップ */

    BinaryTree *root = nullptr;/* 2分木の初期化 */
    bool isFirst = true;/* 2分木の最初の作成時に使用 */
    do {
        if (!getline(fin, line)) break;/* これ以上の行はないとき無限ループから脱出 */
        vector <string> s = ::split(line,',');/* ','でsplitし、文字列(string)をゲット */
        name = s[0];
        GI = stof(s[1]);/* GI: string -> float */
        carbon = stof(s[2]);/* carbon: string -> float */
        calorie = stof(s[3]);/* calorie: string -> float */
        fat = stof(s[4]);/* fat: string -> float */
        protein = stof(s[5]);/* protein:string -> float */
        Food food(name,GI,carbon,calorie,fat,protein);/* 食べ物クラスのオブジェクト生成 */
        cout << name.c_str() << " " << GI << " " << carbon
    	    << " " << calorie << " " << fat<< endl; /* 標準出力coutに書き出し */
        if (isFirst){
    	    root = new BinaryTree(food);/* 2分木のルートノードを生成 */
    	    isFirst = false;
        }
        else
    	    root = root->insert(root, food);/* ルート以外のノードを生成 */
    } while (true);
    fin.close(); /* ファイルのclose */
    cout << endl << "カロリーの小さい順にプリントします..." << endl;
    root->printInOrder(root);/* 2分木を中順序でプリント */
    root->deleteTree(root);/* メモリの解放 */
    return 0;
}

main()関数に関してコメントします。 4行目の<fstream>はファイルストリームです。 これにファイル入出力が含まれています。 25行目のstringでコマンドラインからの第一引数でファイル名(食べ物のカロリー数などが含まれるファイル)を入力し、 26行目のifstreamでfinという名前にしてファイルをオープンしています。 実際の読み込みは、43行目のgetline関数で、 これがfalse、すなわち、もう読み込むデータがないとき、無限ループから脱出します。

ループ内の51行目では、Foodクラスのオブジェクトを生成しています。 ここでは、ポインタは使っていませんので、foodという変数にクラスそのものが、 割り当てられます。この文を

Food *food = new Food(name,GI,carbon,calorie,fat);

としても問題ありません。 ただし、その場合、55行目と59行目のfoodは*foodとする必要があります。 55行目はBinaryTreeクラスのルート要素を生成するための処理です。 2番目以降のデータに関しては、すでにルートがあるため、 insert()メソッド(関数)を呼び出すように変更しています。 ループから脱出したら、61行目でファイルをcloseしています。 その後、63行目で2分木のルートから再帰的に中順序でカロリーの低い順に読み込んだ食べ物をプリントする関数を呼び出しています。 64行目は再帰的に木を削除しています。

データの保護の観点から見直してみよう

データ保護の観点から、このプログラムを 見直しましょう。最初に、Foodクラスをstructで定義し、 データ変数すべてをpublicとしました。これを見直すことを考えます。 まず、structをclassと変更します。そうすると、まずデータ変数はすべてprivateになります。 これだけだと、逆にアクセス制御がかかり不便になります。 そこで、新たに、publicな関数を用意し、mainなどの外部からは、 データ変数の種類と型は教えるとしても、変数名は教えないとします。 外部から値を参照したい変数に関してだけその値を返す関数をpublicで用意することを考えます。 具体的には、「食べ物の名前を返す関数」としてgetName()と、 「食べ物の(100gあたりの)カロリーを返す関数」としてgetCalorie()(カロリー値が2分木の分岐におけるキーであるため)という関数の2つだけ公開することとします。 そこで、以下のようなクラスを用意すればいいことがわかります。


Food.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
/* プログラム3-1-2(classバージョン) 食べ物クラス */
class Food {
  private:
	string name; /* 食べ物の名前 */
	float GI; /* GI値 */
	float carbon; /* 炭水化物 */
	float calorie; /* カロリー */
	float fat; /* 脂質 */
	float protein; /* タンパク質 */
  public:
	Food(){}
	Food(string name, float GI, float carbon,
		float calorie, float fat, float protein ): name(name), GI(GI),
		carbon(carbon), calorie(calorie), fat(fat), protein(protein) {}
	string getName(){ return name; }/* 名前を返す関数*/
	float getCalorie(){ return calorie; }/* キー(カロリー値)を返す関数 */
};


この変更が他にどのような影響を与えたかを考えてみます。 まず、プログラム3-2で与えたBinaryTree.hの幾つかでFoodクラスのデータ変数にpublicにアクセスできることを仮定していた部分があり、これを修正する必要があります。 main関数のあるプログラム3-3に関しては、何も変更することはありません。 端末から実行したサンプルは以下のようになします。 ここで、 Linux環境下でよく使われるMakefileを、上記で述べたFoodクラスとBinaryTreeクラスの組み合わせに適用します。
Makefile
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#
# Makefile ソフトウェア演習(アドバンスコース)
#
# $ make  コンパイルして(ライブラリなしで)実行モジュールを作成
# $ make clean オブジェクトファイルを削除
# 
#
# for C++ define  CC = g++
C++ = g++
CFLAGS  = -Wall -std=c++14
OFILES = BinaryTree.o FoodTree.o
default: FoodTree

FoodTree:$(OFILES) FoodTree.cpp
	$(C++) $(CFLAGS) -o FoodTree $(OFILES)

# 依存関係
BinaryTree.o:  BinaryTree.h BinaryTree.cpp
	$(C++) $(CFLAGS) -c BinaryTree.cpp
FoodTree.o: FoodTree.cpp Food.h
	$(C++) $(CFLAGS) -c FoodTree.cpp
#
clean: 
	$(RM) *.o

このMakefileをもとに、makeを実行し、プログラム3-4で実行した結果の一部は以下の通りです。
$ make
g++ -Wall -std=c++14 -c BinaryTree.cpp
g++ -Wall -std=c++14 -c FoodTree.cpp
g++ -Wall -std=c++14 -o FoodTree BinaryTree.o FoodTree.o
$ ./FoodTree food.csv
白米 81 37.1 168 0.3
食パン 91 46.7 264 4.4
そば 54 26 132 1
うどん 85 21.6 105 0.4
もち 80 50.3 235 0.8
...
カロリーの小さい順にプリントします...
もずく(4)
しいたけ(5)
松茸(11)
...
フライドポテト(388)
キャラメル(433)
チョコレート(557)

コピーコンストラクタ

オブジェクト指向言語の特徴の一つがクラスによるデータ保護、関数保護の機能でした。 もうひとつ、コンストラクタ(構築子)という、 関数の型がなく、関数名がクラス名と一致する関数を用意する必要がありました。 この中に、C++言語では、更に、 コピーコンストラクタという概念があります。 プログラム3-2のBinaryTreeクラスに例があります。 Foodクラスでも同様に定義することはできます。 基本的には、他のコンストラクタと同様に、戻り値はなく、 関数名はクラス名と等しくなります。違うのはパラメータが

const クラス名 &仮引数名

という形態をとる点です。 仮引数を参照変数として&をつける点と、 頭にconstをつける点がコピーコンストラクタ特有の記法となります。 どういう時に、コピーコンストラクタが呼び出されるかというと、以下のような場面です。

BinaryTree root2 = root1;

すなわち、新たなBinaryTreeクラスの変数に、すでに定義した変数を代入することで宣言する場合です。 このような場合、C++コンパイラは、 root2に対してデフォルトの無引数のコンストラクタを呼び出すのではなく、 コピーコンストラクタを自動的に呼び出します。 通常、コピーコンストラクタは定義されない場合、システムは、 勝手に個々のデータ変数をコピーします。 問題は、それ以外の処理をプログラマがしたい場合、 コピーコンストラクタを自分で作成する機能を備えています。 ちなみに、Java言語では、上の例に示したような定義文の代入だけでは、コピーは起こらず、root1のアドレスがroot2に渡されるだけですので注意してください。

ソフトウェア工学的観点から考えよう

オブジェクト指向言語のよさは、 利用者であるユーザに製作者であるプログラマが詳細をいい意味で「隠蔽」できることにあります。 上の例でFoodクラスのデータ変数をprivateにすることで、 仮にファイルからのデータ読み込み部分も利用者は知らないと仮定すると、 食べ物の名前と100グラムあたりのカロリーだけがわかるというインタフェースを提供したことで、 使わなかったGI値、炭水化物の量、脂質の量は「隠蔽」されたことになります。 また、privateにあるprintメンバー関数も、利用者は知る必要がなく、 公開されているprintInOrder関数の仕様だけわかればいいことになります。

特別なことがない限り、クラス内のメンバー変数(データ変数)は、 privateで定義することが一般的です。 これは、そもそもアクセス制御子を付けない場合、暗黙的にprivateとなることからも推測されます。 また、 クラスの定義の中でメンバー変数を( double value = 0; のように)初期化できません。 どうしても初期化したい場合は、すでに例を述べたように、コンストラクタの中や、 初期化リストで行うことができます。 ただし、コンストラクタの引数を代入するのではなく、 特定の「値」で初期化したい場合は、変数にconst修飾子がついている場合に限られますので注意してください。 一方、メンバー関数の実体は、クラスの定義内に書くことができます。 このような関数は、コンパイラによって、inline関数として扱われます。

メンバー関数をクラスの定義内で実装することは、その実体が小規模である場合は有効とされています。 実際、クラスの定義内にメンバー関数を記述してinline関数とし、 関数呼び出しのスタックが不要なためオーバーヘッドを抑えることができますが、 一方でコンパイラは関数をそのままメモリに書き出すのでメモリが増大する傾向にあり、 キャッシュミスを引き起こすため、結果としてオーバーヘッドが大きくなる事例も報告されています。

メンバー関数がある程度多くなり、その実装コードが数行では書ききれないような場合は、 C++言語では、以下のイラストで描いているようにクラスを定義するヘッダーファイルとメンバー関数を定義するファイルを分離して実装することが、 オブジェクト指向プログラミングのデザイン論からは推奨されています。

  1. クラス(myClass.h)をヘッダーファイルとして設計する。メンバー関数はできるだけプロトタイプだけとする。
  2. メンバー関数は、クラスの外部に別ファイル(myClass.cpp)として設計する。
  3. main関数や外部のクラス等からmyClassを呼び出して利用する。
というやり方です。図で表現すると、以下のようになります。



ただし、templateを使う場合はデータ型をコンパイル時に決定するため、 .cppと.hを分離することはできません。 その代り、ファイルとしては分離できます。 具体的には、#ifndefのいつものやり方で、 ヘッダーファイル(たとえば、"myTemplate.h")を 1度しか読み込みさせないのは勿論のこととして、 さらに、ヘッダーファイルの末尾に、

#include "myTemplate.cpp"

のように、クラス内の関数を定義しているプログラム自体も、 #includeしておく必要があります。 つまり表面的な分離をしておき、実際は#includeを使って合体して利用することになります。 templateによる便利さの代償というところでしょうか。 他にも、幾つか方法がありますが、templateを使って、ヘッダーファイルと、 クラス内の関数群を定義するファイルを分離する場合は、注意してください。

ちなみに、Javaではヘッダーファイルという概念はなく、クラスの定義部と実装部を分離できないため、 そもそもクラスファイルごとに肥大化しないように設計する必要があります。 C#言語では、ヘッダーファイルはありませんが、public partial class A { public void DoWork(){} }をどこかに定義し、 public partial class A { public void GoToLunch(){} }を別途定義するといった、 partialというキーワードが用意されていて部分クラスという独自の概念に基づくモジュラープログラミングが可能なため、 小刻みにクラスを分割し、効率をあげる別な方法が用意されています。

C++言語の変数のスコープ

C++言語では、クラスという概念が入ってきました。 これに伴い、変数には、クラススコープがあることを既にこれまでの資料で簡単に触れました。 その際、クラススコープのほかに、名前空間スコープがあることも触れました。 これらの変数のスコープに関して、ここでまとめておきます。

C++の変数のスコープ

スコープ名 主な内容
クラススコープ クラス内のデータ変数(メンバー)とメンバー関数
グローバルスコープ クラス外で定義される大域的な変数、定数、関数
関数スコープ 関数内で定義された(ローカル)変数、ラベル
ブロックスコープ ブロック({...})で定義された局所的な変数
名前空間スコープ namespace {...}で定義された変数、定数、関数

C++言語のクラス定義例(Part 3)

数値計算や空間のデータ解析、物理シミュレーション等で必須となる行列クラスを作ってみます。 将来、演算子のオーバーローディングを学ぶと、よりすっきり書くことができますが、 ここでは演算子のオーバーローディングなしで作成しています。

行列クラス

まず、ヘッダーファイルである行列のクラスを以下のように作ります。 行列データは2次元配列のようにアクセスできる、 double型を要素として持つvector型のvector型として定義しています。 なお、STLの代表例であるvector型を詳しく知りたい人は、英語のページですが、 cplusplus.comのこちらのサイトを参照してください。 少しだけコメントすると、vector型では、単純型(int, doubleなど)変数の可変長配列として仕えるだけでなく、任意のクラスの変数の可変長配列として利用できます。 データの追加はvector型に備わっているpush_back関数で動的にデータを追加していくか、もしくは、

vector型の話はひとまず置いておき、行列クラスの話に戻りましょう。 ここでは、行列の要素をdouble型を要素として持つvector型のvector型(<vector < vector < double> >)として定義しましたが、 別な実装例とてしては、double **型などでもほぼ同様の定義が可能です。
myMatrix.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* プログラム3-5 行列クラス例 */
#ifndef _MYMATRIX
#define _MYMATRIX
#include <iostream>
using namespace std;
#include <vector>

class Matrix {
	private:
		int row; /* 行のサイズ */
		int col; /* 列サイズ */
		vector <vector <double> > v;/* 行列の要素 */
	public:
		Matrix(int m, int n);/* コンストラクタ */ 
		Matrix(const Matrix &mat);/* コピー・コンストラクタ */ 
		void print(void);/* プリント関数:プロトタイプ */
		Matrix* multiply(Matrix &m);/* 乗算:プロトタイプ this->matrix*m */
		vector <double> *multiply(vector <double> &vec);/* 乗算 */
		Matrix* transpose();/* 転置行列:プロトタイプ */
		vector <double> GaussElimination();/* ガウス消去法 */
		vector <double> getRow(int i){ return v[i]; }/* 行を返す */
		double get(int i, int j){ return v[i][j]; }/* (i,j)要素を返す */
		void   set(int i, int j, double x){ v[i][j] = x; }/* (i,j)要素をセットする */
};
#endif
次に、行列のメンバー関数を逆行列以外の部分(myMatirx.cpp)と、 ガウス消去法で求めるprivate関数を含む部分(myGaussElimination.cpp) に分けることにします。


myMatrix.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/* プログラム3-6 行列クラス(メンバー関数)*/
#include <iostream>
using namespace std;

#include "myMatrix.h"

/* コンストラクタ */
Matrix::Matrix ( int m, int n ) :v(m) {//行サイズに0で初期化
	row = m; col = n; 
	for (auto i = 0 ; i < row; i++)//列は動的にメモリ確保
		v[i].resize(col);
}

/* コピーコンストラクタ */
Matrix::Matrix ( const Matrix &A ): v(A.row) {
	row = A.row; col = A.col; 
	for (auto i = 0 ; i < row; i++)//列は動的にメモリ確保
		v[i].resize(col);
	for (auto i = 0 ; i < row; i++)
		for (auto j = 0 ; j < col ; j++)
			v[i][j] = A.v[i][j];
}

/* 行列の要素のプリント */
void 
Matrix::print() {
    int n = v.size();// should be (n == row)
    for (auto i=0; i < row; i++) {
        for (auto j=0; j < col; j++) {
            cout << v[i][j] << " ";
            if (j == n-1)  cout << "| ";
        }
        cout << endl;
    }
    cout << endl;
}

/* 乗算:C = this->matrix * A */
Matrix *
Matrix::multiply(Matrix &A){/* 行列の乗算 */
	int M = this->row;
	int K = this->col;
	int N = A.col;
	if (K != A.row){
		cout << "2つの行列は乗算ができません: " <<
			" 左行列の列サイズ = " << K <<
			" 右行列の行サイズ = " << A.row << endl;
		return nullptr;
	}
	Matrix *C = new Matrix(M, N);/* 結果格納用 */

	/* 初期化 */	
	for ( auto i = 0 ; i < M ; i++ )
		for ( auto j = 0 ; j < N ; j++ ) C->v[i][j] = 0.0;
	/* 乗算本体 */
	for ( auto i = 0 ; i < M ; i++ )
		for ( auto k = 0 ; k < K ; k++ )
			for ( auto j = 0 ; j < N ; j++ )
				C->v[i][j] += this->v[i][k] * A.v[k][j];
	return C;
}

/* 転置行列 */
Matrix *
Matrix::transpose(){/* 行列の転置 */
	int M = this->row;
	int K = this->col;
	Matrix *C = new Matrix(K, M);/* 結果格納用 */

	/* 初期化 */	
	for ( auto i = 0 ; i < K ; i++ )
		for ( auto j = 0 ; j < M ; j++ ) 
			C->v[i][j] = 0.0;
	/* 乗算本体 */
	for ( auto i = 0 ; i < M ; i++ )
		for ( auto k = 0 ; k < K ; k++ )
			C->v[k][i] = this->v[i][k];
	return C;
}

/* ベクトルとの乗算:w = matrix * v */
vector <double> *
Matrix::multiply(vector <double> &vec){/* 行列の乗算 */
	int M = this->row;
	int K = this->col;
	vector <double> *w = new vector<double>(M);/* 結果格納用 */
	int N = (int)vec.size();
	if (N != K){
		cerr << "行列の列のサイズ:"<v[i][j] * vec[j];
			(*w)[i] = t;
		}
	return w;
}

上述のプログラムでMatrixのコンストラクタで vector < vector <double>> v;で定義されていた変数の初期化を行っています。 その部分は、たとえば、以下のように置き換えても結構です。
Matrix::Matrix ( int m, int n ){
	row = m; col = n; 
	vector <vector <double>> x(row, vector<double>(col));
	v = x;
}
一種の2次元の動的な配列でdouble型でrow x colのサイズの領域を確保しています。

ガウス消去法で方程式の解を求める部分は以下のようです。 英語のWikipedia にかなり丁寧にGaussian eliminationが載っていますので興味あれば参照ください。

myGaussElimination.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* プログラム3-7 行列クラス(ガウス消去法)*/
#include <iostream>
using namespace std;
#include <cmath>
#include "myMatrix.h"

vector <double>
Matrix::GaussElimination () {// should be (row+1 == col)
    int n = v.size();

    for (auto i = 0 ; i < n ; i++ ) {
        // Search for maximum in this column
        double maxE = abs(v[i][i]);
        int maxRow = i;
        for (auto k = i+1 ; k < n ; k++) {
            if (abs(v[k][i]) > maxE) {
                maxE = abs(v[k][i]);
                maxRow = k;
            }
        }

        // Swap maximum row with current row (column by column)
        for (auto k = i ; k < n+1 ; k++) {
            double tmp = v[maxRow][k];
            v[maxRow][k] = v[i][k];
            v[i][k] = tmp;
        }

        // Make all rows below this one 0 in current column
        for (auto k = i+1 ; k < n ; k++) {
            double c = -v[k][i]/v[i][i];
            for (auto j = i ; j < n+1 ; j++ ) {
                if (i==j) {
                    v[k][j] = 0;
                } else {
                    v[k][j] += c * v[i][j];
                }
            }
        }
    }

    // Solve equation Ax=b for an upper triangular matrix A
    vector <double> x(n);
    for (auto i = n-1 ; i >= 0; i--) {
        x[i] = v[i][n]/v[i][i];
        for (auto k = i-1 ;k >= 0 ; k-- ) {
            v[k][n] -= v[k][i] * x[i];
        }
    }
    return x;
}

最後にmain関数例を示します。
myMatrixMain.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/* プログラム3-8 行列のメインプログラム */
#include <iostream>
using namespace std;
#include "myMatrix.h"
#include <iomanip>

int main(void){
	int m=4, n = m+1;
    	/* E1:   X1 +  X2            + 3*X4 =  4
       	   E2: 2*X1 +  X2   -  X3    +   X4 =  1
       	   E3: 3*X1 -  X2   -  X3    + 2*X4 = -3
       	   E4:  -X1 + 2*X2 + 3*X3    -   X4 =  4
    	*/ 
	double B[m][n] = {
  	 {1.0, 1.0, 0.0, 3.0, 4.0},
   	 {2.0, 1.0, -1.0, 1.0, 1.0},
   	 {3.0, -1.0, -1.0, 2.0, -3.0},
   	 {-1.0, 2.0, 3.0, -1.0, 4.0}};

	Matrix A(m,n);//与えられたデータから作成する行列クラスのオブジェクト
	for ( auto i = 0 ; i < m ; i++)
		for ( auto j = 0 ; j < n ; j++ ) A.set(i, j, B[i][j]);
	cout << "与えられた行列(方程式の係数と値の拡大行列)は以下のとおりです" << endl;
	A.print();/* 元の行列 */
	vector <double> x(m);// 出力される解を保持
	Matrix C = A;//内容が破壊される前にコピーコンストラクタで保持
	x = A.GaussElimination();// call Gaussian elimination
	/* 方程式の解 */
	cout << "方程式の解は以下のとおりです" << endl;
	for (auto i=0; i < m ; i++)
		cout << fixed << setprecision(3) << x[i] << " ";
	cout << endl << endl;
	
	// 行列の乗算例
	Matrix D(n,n);
	Matrix *T = C.transpose();
	Matrix *M = T->multiply(C);
	delete T;// ポインタでnewしたメモリは解放する
	D = *M;// D = t(C) * C
	cout << "乗算の結果は以下のとおりです" << endl;
	D.print();/* 乗算結果の行列 */
	delete M;// ポインタでnewしたメモリは解放する
	return 0;
}
9行目から13行目のコメントを見てわかるように、 連立1次方程式の解を求めるのに、行列クラスを使っています。

上記の例のように複数のプログラムに分かれている場合は、2分木でも示したように、 Makefileを使うのが便利です。
Makefile
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#
# Makefile ソフトウェア演習(アドバンスコース)
#
# $ make  コンパイルして(ライブラリなしで)実行モジュールを作成
# $ make clean オブジェクトファイルを削除
# 
#
# for C++ define  CC = g++
C++ = g++
CFLAGS  = -Wall -std=c++14
OFILES = myMatrix.o myGaussElimination.o myMatrixMain.o
default: myMatrixMain

myMatrixMain:$(OFILES) myMatrixMain.cpp myMatrix.h
	$(C++) $(CFLAGS) -I. -o myMatrixMain $(OFILES)

# 依存関係
myMatrix.o:  myMatrix.cpp myMatrix.h
	$(C++) $(CFLAGS) -c myMatrix.cpp
myGaussElimination.o: myGaussElimination.cpp myMatrix.h
	$(C++) $(CFLAGS) -c myGaussElimination.cpp
myMatrixMain.o: myMatrixMain.cpp myMatrix.h
	$(C++) $(CFLAGS) -c myMatrixMain.cpp
#
clean: 
	$(RM) *.o
run:
	./myMatirxMain

このMakefileをもとに、makeを実行し、main関数を含むプログラム3-8で実行した結果が以下の通りです。

$ make
g++ -Wall -std=c++14 -c myMatrix.cpp
g++ -Wall -std=c++14 -c myGaussElimination.cpp
g++ -Wall -std=c++14 -c myMatrixMain.cpp
g++ -Wall -std=c++14 -I. -o myMatrixMain myMatrix.o myGaussElimination.o myMatrixMain.o
$ ./myMatrixMain
与えられた行列(方程式の係数と値の拡大行列)は以下のとおりです
1 1 0 3 | 4
2 1 -1 1 | 1
3 -1 -1 2 | -3
-1 2 3 -1 | 4

方程式の解は以下のとおりです
-1.000 2.000 -0.000 1.000

乗算の結果は以下のとおりです
15.000 -2.000 -8.000 12.000 -7.000 |
-2.000 7.000 6.000 0.000 16.000 |
-8.000 6.000 11.000 -6.000 14.000 |
12.000 0.000 -6.000 15.000 3.000 |
-7.000 16.000 14.000 3.000 42.000 |

ガウス消去法の応用例

ガウスの消去法の応用例として,以下のような回路を流れる電流を求める問題を解いてみましょう。



キルヒホッフの法則から、以下の5つの式が得られます。

E1: 5i1 +  5i2 =  V
E2: 5i2 - 7i3 -  2i4 =  0
E3: 2i4 -  3i5 = 0
E4: i1 - i2 - i3 =  0
E5: i3 - i4 - i5 =  0

たとえば電圧V=100(v)として、ガウスの消去法を適用してみましょう。 myMatrix.cppとほぼ同じ内容で以下の部分を変更したものを作成します。
    double B[5][6] = {
        {5.0, 5.0, 0.0, 0.0, 0.0, 100.0},
        {0.0, 5.0, -7.0, -2.0, 0.0, 0.0},
        {0.0, 0.0, 0.0, 2.0, -3.0, 0.0},
        {1.0, -1.0, -1.0, 0.0, 0.0, 0.0},
        {0.0, 0.0, 1.0, -1.0, -1.0, 0.0}};
あとは、これで実行すれば、電流の値が求まります。
行列に関しては、演算子のオーバーローディングとして、 近い将来、別なクラス定義を紹介します。

数値積分クラス

もうひとつ別の例を紹介します。 クラスを定義して、数値積分を行う例です。 その際、積分の下限値、上限値や、積分を細かい区間に刻んで加算として近似する際の刻み総数などをprivateなメンバー変数として定義することにします。 また、関数のポインタを用い、 数学関数sin, cos, tanのほか、カスタム的に作成したinverseという名前の関数f(x) = 1/(x+1)に関しても定積分してみます。 まず、クラスファイル(ヘッダーファイル)は以下のように実装することにします。
Integral.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*プログラム3-9、数値積分クラス */
#include <iostream>
using namespace std;
using Fptr = double (*)(double);//関数へのポインタのエイリアス
class Integral {
  private:
    double min; /* 定積分の下限 */
    double max; /* 定積分の上限 */
    double stepMax;/* 刻み総数(> 0) */
    Fptr f;/* 被積分関数 */
    bool debug; /* デバグ用途中経過書き出しフラグ */
  public:
    Integral(double min, double max, double stepMax, Fptr f): 
        min(min), max(max), stepMax(stepMax), f(f) {/* コンストラクタ */
            debug = true;
    }
    ~Integral(){}/* デストラクタ */
    void setDebug(bool flag){ debug = flag; }
    void compute(double *result, int *rc);/* rc: return code */
};

void
Integral:: compute(double *result, int *rc){
    double sum = 0.0;
    double x;
    if (debug){
        cout << "定積分を開始します..." << endl;
        cout << "   下限値:" << min << endl;
        cout << "   上限値:" << max << endl;
        cout << "   刻み総数:" << stepMax << endl;
    }
    if (stepMax <= 0.0) {
        *result = -1.0;/* 刻み総数が負だった */
        *rc = -1;/* Return Code (rc)をゼロ以外とする */
        return;
    }
    for (auto i = 0 ; i < stepMax ; i++ ){
        x = ( i /stepMax ) * ( max - min ) + min;
        sum += f(x) * ( max - min ) / (stepMax + 1.0);
    }
    *result = sum;
    *rc = 0;
    return;
}
C++言語では、ヘッダーファイルにクラスを定義するのが通例です。 これを呼び出すmain関数を含むプログラム例は以下のようです。
IntegralMain.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*プログラム3-10、数値積分クラスを呼ぶmain関数 */
#include "Integral.h"
#include <cmath>
#include <cstdlib>

#define MAXFUNC (4)
double inverse(double x){ return (1.0/(x+1.0)); }

int main(int ac, char **av){
  Fptr f[MAXFUNC] = {cos, sin, tan, inverse};/* 関数へのポインタ */
  string s[MAXFUNC] = {"cos", "sin", "tan", "inverse"};/* 関数名の文字列 */
  double result;/* 結果保持 */
  int rc;/* リターンコード */
  double min = atof(av[1]);/* 下限の読込 */
  double max = atof(av[2]);/* 上限の読込 */
  double stepMax = atof(av[3]);/* 刻み幅総数の読込 */
  cout << "定積分の下限値を0、上限値をpi/4として計算します" << endl;
  for (auto i = 0 ; i < MAXFUNC ; i++){
    Integral *integral = new Integral(min, max, stepMax, f[i]);/* オブジェクト生成 */
    integral->compute(&result, &rc);/* 数値積分計算 */
    if (rc == 0)
      cout << "\t " << s[i] << "の数値積分結果は" << result << "です" << endl;
    delete integral;/* デストラクタを呼び出す */
  }
  return 0;
}
実行結果は以下のようです。

$ g++ -Wall -std=c++14 -o Integral IntegralMain.cpp

$ ./Integral 0.0 0.78539816 100
定積分の下限値を0、上限値をpi/4として計算します
定積分を開始します...
   下限値:0
   上限値:0.785398
   刻み総数:100
         cosの数値積分結果は0.701241です
定積分を開始します...
   下限値:0
   上限値:0.785398
   刻み総数:100
         sinの数値積分結果は0.287242です
定積分を開始します...
   下限値:0
   上限値:0.785398
   刻み総数:100
         tanの数値積分結果は0.339259です
定積分を開始します...
   下限値:0
   上限値:0.785398
   刻み総数:100
         inverseの数値積分結果は0.575616です

using Fptr = の構文はC++11以降で使えるようになったため、-std=c++14(-std=c++11でも同様)のオプションがあることに注意してください。

実世界の身近な例をクラスで作ってみよう

クラスの例として、 『データ構造とアルゴリズム』にたとえて、幾つかの事例を紹介しました。 ここでは、実世界でありそうなオブジェクトをクラスで定義する例を述べます。 具体的には、「車」(Carクラス)を考えます。 オブジェクト指向プログラミングでは、オブジェクトを具現化する「クラス」で、どんな属性(データメンバー)が必要か、 また、その属性にどのような操作(メンバー関数)が必要かを考えます。 「車」には、属性として、メーカー、製造年、車種、燃料タイプ、 馬力、定価、色、プレート番号、最高速度など様々な属性がありえます。たとえば、以下のように定義するとしましょう。

class Car {
  public:
    Car(){}/* コンストラクタ */
    Car(string plateNumber, string maker, int year, 
      double distance, double price);/*コンストラクタ */
    Car(const Car &car);/* コピーコンストラクタ */
    ~Car();/* デストラクタ */
    void print();/* 定義されているメンバー変数をプリント */
    double calculateValue();/* 現在の価格を計算 */
    void setPrice( double price );/* 販売価格の設定 */
    double getPrice(); /* 販売価格の入手 */
 private:
    string type; /* 車種 */
    string maker; /* メーカー */
    string plateNumber; /* プレートナンバー */
    double kph; /* 時速(KPH) kilometer per hour */
    double horsePower; /* 馬力 */
    enum { gas, hybrid, electric } energy;/* 燃料 */
    int year; /* 製造年 */
    string color; /* 色 */
    double price; /* 販売価格 */
    double length; /* サイズ:前後 */
    double width; /* サイズ:左右 */
    double height; /* サイズ:高さ */
    double distance;/* 走行距離 */
};

次に、Carクラスのメンバー関数を定義していきます。

/* Carクラスのメンバー関数の定義 */
Car:: Car(string plateNumber, string maker, int year, 
	double distance, double price){/* コンストラクタ */
	this->plateNumber = plateNumber;/* プレート番号 */
	this->maker = maker; /* メーカー 例:"Toyota" */
	this->year = year;/* 製造年 */
	this->distance = distance;/*走行距離*/
	this->price = price;
	energy = gas;
}

Car::~Car(){/* デストラクタ */
	cout << "\"" << plateNumber << "\"のデストラクタが呼ばれました" << endl;
}

void
Car::print(){/* プリント関数 */
	cout << "-------------------------------------" << endl;
	cout << "メーカー: " << maker << endl;
	cout << "プレート番号: " << plateNumber << endl;
	cout << "製造年: " << year << "年" << endl;
	cout << "走行距離:" << distance << " km" << endl;
	cout << "購入価格 = \\" << 
		fixed << setprecision(1) << setw(8) << price << "円" <price = price;
}

最後に、この「車」オブジェクトを生成(インスタンス作成)して利用することを考えましょう。 C++はJava言語と異なり、オブジェクトの生成は、ポインタによるnew演算子での動的な生成のほか、静的に宣言することでもできます。 たとえば、以下の例はmyCar変数を「車」オブジェクトのインスタンスとして生成しています。


/* 非ポインタでオブジェクトを生成する場合 */
Car myCar1("豊橋 900 ね 53-00", "Toyota", 2004, 
	67000.0, 1800000);/* オブジェクト生成法1 */
myCar1.print();

一方、Carクラスをポインタで定義して、インスタンスを new演算子で生成することもできます。


/* ポインタでオブジェクトを生成する場合 */
Car *myCar2 = new Car("浜松 500 あ 24-83","Suzuki", 2010, 
	18000.0, 1500000);/* オブジェクト生成法2 */
myCar2->print();

まとめると、クラスの定義を含むヘッダー部分は以下のように書けます。
Car.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/* プログラム3-11 車クラス */
#include <iostream>
using namespace std;
#include <string>
#include <iomanip>

class Car {
	public:
		Car(){}/* コンストラクタ */
		Car(string plateNumber, string maker, int year, 
			double distance, double price);/*コンストラクタ(一部のメンバー変数のみ) */
		Car(const Car &car);/* コピーコンストラクタ */
		~Car();/* デストラクタ */
		void print();/* 定義されている属性(メンバー変数)をプリント */
		double calculateValue();/* 現在の価格を計算 */
		void setPrice( double price );/* 販売価格の設定 */
		double getPrice(); /* 販売価格の入手 */
	private:
		string type; /* 車種 */
		string maker; /* メーカー */
		string plateNumber; /* プレートナンバー */
		double kph; /* 時速(KPH) kilometer per hour */
		double horsePower; /* 馬力 */
		enum { gas, hybrid, electric } energy;/* 燃料 */
		int year; /* 製造年 */
		string color; /* 色 */
		double price; /* 販売価格 */
		double length; /* サイズ:前後 */
		double width; /* サイズ:左右 */
		double height; /* サイズ:高さ */
		double distance;/* 走行距離 */
};

/* Carクラスのメンバー関数の定義 */
Car:: Car(string plateNumber, string maker, int year, 
	double distance, double price){/* コンストラクタ */
	this->plateNumber = plateNumber;/* プレート番号 */
	this->maker = maker; /* メーカー 例:"Toyota" */
	this->year = year;/* 製造年 */
	this->distance = distance;/*走行距離*/
	this->price = price;/* 販売価格 */
	energy = gas;
}

Car::~Car(){/* デストラクタ */
	cout << "\"" << plateNumber << "\"のデストラクタが呼ばれました" << endl;
}

void
Car::print(){/* プリント関数 */
	cout << "-------------------------------------" << endl;
	cout << "メーカー: " << maker << endl;
	cout << "プレート番号: " << plateNumber << endl;
	cout << "製造年: " << year << "年" << endl;
	cout << "走行距離:" << distance << " km" << endl;
	cout << "購入価格 = \\" << 
		fixed << setprecision(1) << setw(8) << price << "円" <this->price = price;
}
コンストラクタで初期化するメンバー変数が一部の場合、 初期化リストは使えないので、個別に thisポインタで初期化しています。 これを呼び出すmain関数を含むプログラムは 以下のようです。
Car.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
/* プログラム3-12 車クラスのオブジェクト呼び出し例 */
#include "Car.h"

int main(){
	/* 非ポインタでオブジェクトを生成する場合 */
	Car myCar1("豊橋 900 ね 53-00", "Toyota", 2004, 
		67000.0, 1800000);/* オブジェクト生成法1 */
	myCar1.print();

	/* ポインタでオブジェクトを生成する場合 */
	Car *myCar2 = new Car("浜松 500 あ 24-83","Suzuki", 2010, 
		18000.0, 1500000);/* オブジェクト生成法2 */
	myCar2->print();
	delete myCar2;/* これはなくても終了時にデストラクタが呼び出される */

  	return 0;/* ここでdeleteで明示的に削除されたオブジェクト以外のデストラクタが呼ばれる */
}
以下が実行結果例です。

$ g++ -Wall -std=c++14 -o Car Car.cpp
$ ./Car
--------------------------------------
メーカー: Toyota
プレート番号: 豊橋 900 ね 53-00
製造年: 2004年
走行距離:67000 km
購入価格 = \1800000.0円
現在の価値 = \300905.0円

-------------------------------------
メーカー: Suzuki
プレート番号: 浜松 500 あ 24-83
製造年: 2010年
走行距離:18000.0 km
購入価格 = \1500000.0円
現在の価値 = \740850.0円

"浜松 500 あ 24-83"のデストラクタが呼ばれました
"豊橋 900 ね 53-00"のデストラクタが呼ばれました
オブジェクトの生成方法を変えている点と、 デストラクタがいつ呼び出されるかが違う点に注目してください。

クラスのサイズ、コピーコンストラクタ

コンストラクタとして、プログラム3-12には、2つのタイプ、すなわち クラスのオブジェクトを変数としてそのまま定義する方法と、 ポインタとしてnew演算子で定義する2つの方法を示しました。

このようにクラスのオブジェクトを作ることは非常に簡単です。 C言語のcallocやmallocでは構造体のサイズを与えてメモリ割り当てをしていましたので、 ある意味で、構造体のサイズを知りながらメモリの割り当てを行っていました。 C++では、クラスのオブジェクト生成時にサイズを知る必要はありませんが、 クラスのオブジェクトに必要なメモリサイズを知りたいことがあります。 このような場合、C言語でも利用してきた、sizeof演算子を使います。

一方、コンストラクタには、上で例示した2つののほかに、 コピーコンストラクタがあることは、 すでに説明しました。 そこで、まず、以下のようにコピーコンストラクタを定義します。 (これをコピーコンストラクタのオーバーライドと呼びます)。 なお、コピーコンストラクタで注意すべきは、引数のクラス変数自身が 参照変数(&クラス変数名)となっていることです。 理由は、もし参照変数としなければ、コピーしたいはずのコンストラクタが再帰的に呼び出され、 永久ループとなってしまうのを防ぐためで、実際はコンパイルエラーとなります。 また、const修飾子はなくても動作しますが、通常はつけておきます。

/* Carクラスのメンバー関数のコピーコンストラクタ */
Car::Car(const Car &car){
	cout << "コピーコンストラクタが呼ばれました" << endl;
	this->type = car.type; /* 車種 */
	this->maker = car.maker; /* メーカー */
	this->plateNumber= car.plateNumber; /* プレートナンバー */
	this-> year = car.year; /* 製造年 */
	this->price = car.price; /* 販売価格 */
	this->distance = car.distance;/* 走行距離 */
	cout << "コピー時の内容は以下のようです" << endl;
	this->print();
}

また、以下の3つの関数もpublicなメンバー関数としてクラスに追加することにします。

void
Car:: setPlateNumber(string plateNumber){/* プレート番号を設定する関数 */
	this->plateNumber = plateNumber;
}
void
Car:: setYear(int year){/* 製造年を設定する関数 */
	this->year = year;
}
void
Car:: setDistance(double distance){/* 走行距離を設定する関数 */
	this->distance = distance;
}

このとき、sizeof演算子で、2つのCarクラスのオブジェクトと、 コピーコンストラクタによる3つ目のオブジェクトを含むmain関数は以下のように例示できます。
Car2.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* プログラム3-13 車クラス(上述のコピーコンストラクタ等を含む) */
#include "Car.h" /* クラスの定義が拡張されたバージョン */

int main(){
	/* 非ポインタでオブジェクトを生成する場合 */
	Car myCar1("豊橋 900 ね 53-00", "Toyota", 2004, 
		67000.0, 1800000);/* オブジェクト生成法1 */
	cout << "sizeof(Car) = " << sizeof(Car) << endl;
	cout << "sizeof(myCar1) = " << sizeof(myCar1) << endl;

	/* ポインタでオブジェクトを生成する場合 */
	Car *myCar2 = new Car("浜松 500 あ 24-83","Suzuki", 2010, 
		18000.0, 1500000);/* オブジェクト生成法2 */
	cout << "sizeof(myCar2) = " << sizeof(myCar2) << endl;
	cout << "sizeof(*myCar2) = " << sizeof(*myCar2) << endl;
	delete myCar2;/* これはなくても終了時にデストラクタが呼び出される */

	/* コピーコンストラクタで生成する場合 */
	cout << "\n【コピーコンストラクタの呼び出しテスト】" << endl;
	Car myCar3 = myCar1;/* 代入することで初期化→コピーコンストラクタ */
	myCar3.setPlateNumber("湘南 810 も 07-15");/* プレート番号変更 */
	myCar3.setPrice(2650000);/* 初期購入価格変更 */
	myCar3.setYear(2011);/* 製造年変更 */
	myCar3.setDistance(98650);/* 走行距離変更 */
	cout << "【設定変更後】の内容は以下のようです" << endl;
	myCar3.print();

  	return 0;/* ここでdeleteで明示的に削除されたオブジェクト以外のデストラクタが呼ばれる */
}
以下が実行結果例です。

$ g++ -Wall -std=c++14 -o Car2 Car2.cpp

$ ./Car2
sizeof(Car) = 96
sizeof(myCar1) = 96
sizeof(myCar2) = 8
sizeof(*myCar2) = 96
"浜松 500 あ 24-83"のデストラクタが呼ばれました

【コピーコンストラクタの呼び出しテスト】
コピーコンストラクタが呼ばれました
コピー時の内容は以下のようです
-------------------------------------
メーカー: Toyota
プレート番号: 豊橋 900 ね 53-00
製造年: 2004年
走行距離:67000 km
購入価格 = \1800000.0円
現在の価値 = \300905.0円

【設定変更後】の内容は以下のようです
-------------------------------------
メーカー: Toyota
プレート番号: 湘南 810 も 07-15
製造年: 2011年
走行距離:98650.0 km
購入価格 = \2650000.0円
現在の価値 = \1088549.0円

"湘南 810 も 07-15"のデストラクタが呼ばれました
"豊橋 900 ね 53-00"のデストラクタが呼ばれました
上述の実行結果から、Carクラスのサイズは96バイトであり、 ポインタで宣言されたクラス変数のサイズは8バイトであることがわかりましたが、 これは処理系によって異なる場合がありますので注意してください。

参照変数、参照変数の関数引数での利用

C++では、C言語にはなかった参照変数が使えることは、 第1回資料の「関数への参照渡し」 でもswap関数の例で述べました。再掲します。
/* 参照によるswap関数 */
void swap(int &a, int &b){
	int tmp;
	tmp = a;
	a = b;
	b = tmp;
}
/* 呼び出し側 */
...
	int x = 10;
	int y = 20;
	swap(x, y);
	cout << "x = " << x << " y = " << y << endl;
ただし、一般的には、参照変数は、 元の変数の「別名」(alias)としてC++言語に導入されたものですので、 関数のパラメータが知らないうちに変更される場合は使うべきでない、 ということがC++の設計者Bjarne Stroustrup氏の著書(The C++ Programming Language)で記述されています。 引用すると、
To keep a program readable, it is often best to avoid functions that modify their arguments.
Instead, you can return a value from the function explicitly or require a pointer argument.

車クラスの例で、クラス変数、その参照変数、そのポインタ変数でどう違うか見ておきましょう。 以下のプログラムを見てください。
CarArg.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* プログラム3-14: 参照変数とクラス */
#include "Car.h" /* 三角形クラスのヘッダーファイル */

void printClassByCopy(Car car){
	cout << "*****コピー:printByCopy: size = " << sizeof(car) << endl;
	car.print();
}
void printClassByReference(Car &car){
	cout << "*****参照:printByReference: size = " << sizeof(car) << endl;
	car.print();
}
void printClassByPointer(Car *car){
	cout << "*****ポインタ:printByPointer: size = " << sizeof(car) << endl;
	car->print();
}

int main(int ac, char **av){
	Car car1("豊橋 900 ね 53-00", "Toyota", 2004, 
		67000.0, 1800000);/* オブジェクト生成法1 */
	// Car &car2;/* コンパイルエラー */
	Car &car2 = car1;/* 参照型変数は初期化しないといけない */
	// int &ref = 0;/* コンパイルエラー */
	const int &ret = 0;/* 定数を代入する場合はconstが必要 */
	::printClassByCopy(car1);
	::printClassByReference(car2);
	::printClassByPointer(&car1);
	return ret;
}
この例で注目すべき点が幾つかあります。 まず、20行目に書いているように、 参照変数を単に定義することはできません。 定義する場合は、すでに定義されている変数で初期化(コピー)する必要があります。 この際、クラスの参照変数なので、コピーコンストラクタが呼び出されます。 第2に、車クラスとは直接関係ないですが、参照変数に22行目にあるように、 定数で初期化することはできません。定数で初期化する必要がある場合は、 23行目のように、const修飾子を付ける必要があります。 第3に、参照変数へのポインタは定義できません。 ポインタの配列は定義できますが、 参照変数の配列は定義できません。 また、もっとも重要なことですが、 参照変数はオブジェクトではありません。

一方、関数の引数(パラメータ)として参照変数を使うとswap関数のように値を変えることができますが、この例(printClassByReference関数)のように、 特に、プリントするだけの場合、参照変数を使うことで特別な利点が生まれることはありません。 むしろ、printClassByCopyと同じく、sizeof(Car)の大きさだけ、関数に渡す必要があり、 メモリ的には無駄です。メモリを真に節約し、クラス変数をパラメータとして、 軽快に関数を呼び出したいときは、printByPointerのようにポインタにすべきです。 以下が、プログラムの実行結果です。

$ g++ -Wall -std=c++14 -o CarArg CarArg.cpp

$ ./CarArg
コピーコンストラクタが呼ばれました
コピー時の内容は以下のようです
-------------------------------------
メーカー: Toyota
プレート番号: 豊橋 900 ね 53-00
製造年: 2004年
走行距離:67000 km
購入価格 = \1800000.0円
現在の価値 = \300905.0円

*****コピー:printByCopy: size = 96
-------------------------------------
メーカー: Toyota
プレート番号: 豊橋 900 ね 53-00
製造年: 2004年
走行距離:67000.0 km
購入価格 = \1800000.0円
現在の価値 = \300905.0円

"豊橋 900 ね 53-00"のデストラクタが呼ばれました
*****参照:printByReference: size = 96
-------------------------------------
メーカー: Toyota
プレート番号: 豊橋 900 ね 53-00
製造年: 2004年
走行距離:67000.0 km
購入価格 = \1800000.0円
現在の価値 = \300905.0円

*****ポインタ:printByPointer: size = 8
-------------------------------------
メーカー: Toyota
プレート番号: 豊橋 900 ね 53-00
製造年: 2004年
走行距離:67000.0 km
購入価格 = \1800000.0円
現在の価値 = \300905.0円

"豊橋 900 ね 53-00"のデストラクタが呼ばれました
この実行結果からわかるように、参照変数も、通常の変数であっても、ポインタでない場合は、 パラメータにsizeof(クラス名)だけのバイト数が渡されることがわかります。