M.Hiroi's Home Page

Linux Programming

お気楽C++プログラミング超入門

[ PrevPage | C++ | NextPage ]

オブジェクト指向の基礎知識 (後編)

前回に引き続き、C++のオブジェクト指向機能の基本について説明します。それから、クラスを作るときに必要となる演算子の「多重定義」についても説明します。

●変換コンストラクタ

1 引数のコンストラクタは、引数をそのクラスに型変換する働きをもつ、と考えることができます。これを「変換コンストラクタ」と呼びます。変換コンストラクタが定義されていると、暗黙のうちに型変換が行われることがあります。次のリストを見てください。

リスト : 変換コンストラクタ (sample120.cpp)

#include <iostream>
using namespace std;

class Foo {
  double a;
public:
  Foo() : a(0) { }
  Foo(int x) : a(x) { }
  Foo(double x) : a(x) { }
  double get_a() const { return a; }
};

int main()
{
  Foo a(1);
  Foo b = 2;       // Foo b(2);
  Foo c = 1.2345;  // Foo c(1.2345);
  cout << a.get_a() << endl;
  cout << b.get_a() << endl;
  cout << c.get_a() << endl;
}
$ clang++ sample120.cpp
$ ./a.out
1
2
1.2345

コンストラクタ Foo(int) は int を Foo に、Foo(double) は double を Foo に型変換します。変換コンストラクタが定義されていると、Foo b = 2; のように記述することができます。このとき、暗黙のうちに変換コンストラクタが呼び出され、生成されたインスタンスが変数にセットされます。これを「暗黙の型変換」といいます。

暗黙の型変換を無効にしたい場合は、コンストラクタの前に explicit をつけます。たとえば、explicit Foo(double x) : a(x) {} と定義すると、Foo c = 1.2345; はコンパイルエラーになります。この場合、Foo c(1.2345); のように明示的にコンストラクタを呼び出してください。

●クラスと配列

C++はインスタンスを配列に格納することもできます。宣言は次のようになります。

  1. データ型 配列名[大きさ];
  2. データ型 配列名[大きさ] = {初期値, ... };
  3. データ型 配列名[大きさ] = {コンストラクタ(初期値, ...), ... };

1 の場合はデフォルトコンストラクタが必要で、2 の場合は変換コンストラクタが必要になります。3 のように、コンストラクタを明示的に呼び出して初期化することもできます。また、インスタンスへのポインタを格納する配列も定義することができます。この場合、データ型の後ろに * を付けます。

簡単な例を示しましょう。次のリストを見てください。

リスト : クラスと配列 (sample121.cpp)

#include <iostream>
#include <cmath>
using namespace std;

class Point {
  double x, y;
public:
  Point() : x(0), y(0) { }
  Point(double x0, double y0) : x(x0), y(y0) { }
  double distance(const Point&) const;
};

// 距離を求める
double Point::distance(const Point& p) const
{
  double dx = x - p.x;
  double dy = y - p.y;
  return sqrt(dx * dx + dy * dy);
}

int main()
{
  Point p0[] = {
    Point(0, 0), Point(10, 10), Point(100, 100)
  };
  Point* p1[] = {
    new Point(0, 0), new Point(10, 10), new Point(100, 100)
  };
  cout << p0[0].distance(p0[1]) << endl;
  cout << p0[1].distance(p0[2]) << endl;
  cout << p1[0]->distance(*p1[1]) << endl;
  cout << p1[1]->distance(*p1[2]) << endl;
}
$ clang++ sample121.cpp
$ ./a.out
14.1421
127.279
14.1421
127.279

配列 p0 は 3 個の Point を格納しています。配列 p1 は new を使ってメモリ領域を動的に取得し、その返り値 (ポインタ) を格納しています。distance を呼び出すとき、p0 の場合はドットを使い、引数は参照渡しなので p0[1] のように配列の要素を渡します。p1 の場合、要素の値はインスタンスの先頭アドレスなので、distance を呼び出すときは間接メンバ演算子 -> を使い、引数はアドレスではなく *p1[1] のように値を渡します。

●デストラクタ

コンストラクタはインスタンスを生成するときに呼び出される特別なメンバ関数ですが、インスタンスを廃棄するときに呼び出される特別なメンバ関数もあります。それが「デストラクタ (destructor)」です。デストラクタの構文を示します。

~クラス名() { ... }

デストラクタはクラス名の前にチルダ ( ~ ) を付けて表します。引数や返り値はなく、多重定義することもできません。デストラクタが定義されていない場合、コンパイラがデフォルトのデストラクタを生成します。

インスタンスが局所変数や関数の引数に格納されている場合、スコープから抜けた時点で自動的にデストラクタが呼び出されます。配列の場合も同様です。たとえば、関数内で定義された配列 Foo a[4]; は、関数の実行が終了した時点で、要素のデストラクタが呼び出されます。new で動的にメモリ領域を取得している場合は、delete を実行したときにデストラクタが呼び出されます。

たとえば、クラスのメンバ変数がポインタの場合、コンストラクタで動的にメモリ領域を取得するのが一般的です。デフォルトのデストラクタは動的に取得したメモリ領域を解放しないので、このような場合はデストラクタでメモリ領域を解放する処理をプログラムする必要があります。

簡単な例を示しましょう。

リスト : デストラクタの使用例 (sample122.cpp)

#include <iostream>
using namespace std;

class Foo {
  int* x;
public:
  Foo() : x(new int) {
    *x = 1;
    cout << "Foo " << *x << " create\n";
  }
  Foo(int n) : x(new int) {
    *x = n;
    cout << "Foo " << *x << " create\n";
  }
  ~Foo() {
    cout << "Foo " << *x << " destroy\n";
    delete x;
  }
};

// 大域変数
Foo a;

void foo()
{
  Foo b = Foo(10);
  Foo* c = new Foo(100);
  cout << "call foo\n";
  delete c;
  cout << "return foo\n";
}

int main()
{
  cout << "main start\n";
  foo();
  cout << "main end\n";
}

クラス Foo のメンバ変数 x は int* なので、コンストラクタでメモリ領域を取得してから値を書き込みます。メモリの取得は初期化リストで x(new int) のように行うことができます。デストラクタでは *x を表示してから、delete で取得したメモリ領域を解放します。

クラス Foo のインスタンスは大域変数 a, 関数 foo の局所変数 b とポインタ変数 c にセットします。実行結果は次のようになりました。

$ clang++ sample122.cpp
$ ./a.out
Foo 1 create
main start
Foo 10 create
Foo 100 create
call foo
Foo 100 destroy
return foo
Foo 10 destroy
main end
Foo 1 destroy

大域変数 a の初期化は main 関数を呼び出す前に行われます。関数 foo を呼び出すと、変数 b, c の初期化が行われます。delete c を実行すると、デストラクタが呼び出され、取得したメモリ領域とインスタンスが廃棄されます。

関数 foo の実行が終了すると局所変数 b が廃棄されるので、ここで Foo のデストラクタが呼び出されます。大域変数 c の廃棄は、プログラムの実行が終了するとき、つまり main 関数の実行が終了したあとに行われます。

●コピーコンストラクタ

コンストラクタで動的にメモリ領域を取得する場合、実はデストラクタを用意するだけでは不十分で、「コピーコンストラクタ」と「代入演算子の多重定義」が必要になります。たとえば、次のプログラムを見てください。

リスト : メモリ領域の二重解放 (sample123.cpp)

#include <iostream>
using namespace std;

class Foo {
  int* x;
public:
  Foo() : x(new int) { *x = 1; }
  Foo(int n) : x(new int) { *x = n; }
  // デストラクタ
  ~Foo() { delete x; }
  // メンバ関数
  int get_x() const { return *x; }
  void put_x(int n) { *x = n; }
};

int main()
{
  Foo a;
  Foo b = a;
  cout << a.get_x() << endl;
  cout << b.get_x() << endl;
  a.put_x(123);
  cout << a.get_x() << endl;
  cout << b.get_x() << endl;
 
}

局所変数 b を初期化するとき、初期値に変数 a の値を使っています。C++のクラスはC言語の構造体と同様に、メンバ変数の値をコピーすることができます。変数 a のメンバ変数の値は変数 b にコピーされますが、ここで問題が発生します。実際に試してみましょう。

$ clang++ sample123.cpp
$ ./a.out
1
1
123
123
free(): double free detected in tcache 2
中止

インスタンスをコピーするとき、デフォルトの動作ではメンバ変数 x の値 (参照先データのアドレス) だけがコピーされるので、変数 a と b のメンバ変数 x が同じメモリ領域を指し示すことになります。最初に変数 a のデストラクタが実行されると、メンバ変数 x のメモリ領域が解放されますが、次に変数 b のデストラクタが実行されると、既に解放したメモリ領域を再度解放しようとします。このとき、このようなエラーが発生します。

C++の場合、インスタンスは変数の初期値や関数の引数などに渡したり、関数の返り値としてインスタンスを返すことができます。このとき、新しいインスタンスにメンバ変数の値がコピーされますが、ここで呼び出される特別なメンバ関数が「コピーコンストラクタ」です。クラス Foo のコピーコンストラクタの定義は次のようになります。

Foo(const Foo& obj) { ... }

this ポインタが生成された新しいインスタンスを表し、引数 obj がコピー元のインスタンスを表します。

C++ の場合、インスタンスをコピーするときはコピーコンストラクタが呼び出されます。コピーコンストラクタが定義されていない場合、コンパイラによってデフォルトのコピーコンストラクタが生成されますが、その動作はメンバ変数の値をコピーするだけです。したがって、動的にメモリ領域を取得している場合は、デストラクタと同様にコピーコンストラクタも定義する必要があるのです。

それでは、コピーコンストラクタを定義してみましょう。次のリストを見てください。

リスト : コピーコンストラクタの使用例 (sample124.cpp)

#include <iostream>
using namespace std;

class Foo {
  int* x;
public:
  Foo() : x(new int) { *x = 1; }
  Foo(int n) : x(new int) { *x = n; }
  // コピーコンストラクタ
  Foo(const Foo& a) : x(new int) { *x = *(a.x); }
  // デストラクタ
  ~Foo() { delete x; }
  // メンバ関数
  int get_x() const { return *x; }
  void put_x(int n) { *x = n; }
};

int main()
{
  Foo a;
  Foo b = a;
  cout << a.get_x() << endl;
  cout << b.get_x() << endl;
  a.put_x(123);
  cout << a.get_x() << endl;
  cout << b.get_x() << endl;
  b.put_x(456);
  cout << a.get_x() << endl;
  cout << b.get_x() << endl;
}

コピーコンストラクタでメモリ領域を取得して、そこに引数 a のメンバ変数 x の値 *(a.x) を書き込むだけです。引数 a は参照なので、a.x がポインタ変数を表し、その値を取り出すので *(a.x) と記述します。

それでは実行してみましょう。

$ clang++ sample124.cpp
$ ./a.out
1
1
123
1
123
456

変数 a と b は異なるメモリ領域を取得しているので、put_x で値を書き換えても、他の変数に影響を与えることはありません。デストラクタも正常に動作します。

実をいうと、これだけでは不十分で、次のようなプログラムでは不具合が発生します。

int main()
{
  Foo a = Foo(10);
  Foo b = Foo(20);
  ...
  a = b;   // インスタンスの代入
}

a = b; で b のメンバ変数の値を a のメンバ変数にコピーすることができますが、この処理は「代入」なのでコピーコンストラクタは呼び出されず、代入演算子 = の処理が呼び出されます。この場合もデフォルトの処理では不具合が発生します。これを回避するためには「演算子の多重定義」が必要になります。

●演算子の多重定義

C++は「関数」だけではなく「演算子」も多重定義することができます。演算子の多重定義にはキーワード operator を使います。二項演算子の場合は次のように定義します。

  1. 返り値の型 operator 演算子(左辺式の型 仮引数, 右辺式の型 仮引数) { 本体 }
  2. 返り値の型 operator 演算子(右辺式の型 仮引数) { 本体 }

1 は通常の関数として定義する方法で、2 はクラスのメンバ関数として定義する方法です。1 の場合、a op b は op(a, b) と解釈されます。2 の場合、左辺式はそのクラスのインスタンスになり、this ポインタで参照することができます。つまり、a op b は a.op(b) と解釈されます。

なお、特定の演算子 = [] () -> は 2 の方法でしか定義することができません。これらの演算子は左辺式が必ずインスタンスになります。また、ユーザーが多重定義できない演算子 :: . .* もあります。

簡単な例題として、クラス Point と Point3d で出力演算子 << を多重定義してみましょう。出力演算子は次のように多重定義します。

ostream& operator<<(ostream& output, 右辺式の型 仮引数)
{
  本体;
  ...
  return output;
}

ostream は出力ストリームを表すクラスです。ヘッダファイル iostrem をインクルードすると、自動的に ostream も読み込まれます。標準出力 cout は ostream のインスタンスです。

たとえば、int x の値を出力する場合、cout << x; としますが、これは <<(cout, x) の呼び出しになります。演算子 << は左結合なので、cout << x << endl; は (cout << x) << endl; と解釈されます。したがって、出力演算子で左辺式 (ostream) を返せば、出力演算子をつなげて複数のデータを出力することができます。

プログラムは次のようになります。

リスト : 出力演算子の多重定義 (sample125.cpp)

#include <iostream>
#include <cmath>
using namespace std;

class Point {
  double x, y;
public:
  Point(){ x = 0; y = 0; }
  Point(double x0, double y0) { x = x0; y = y0; }
  double distance(const Point&) const;
  friend ostream& operator<<(ostream&, const Point&);
};

// 多重定義
ostream& operator<<(ostream& output, const Point& p)
{
  output << "(" << p.x << ", " << p.y << ")";
  return output;
}

double Point::distance(const Point& p) const
{
  double dx = x - p.x;
  double dy = y - p.y;
  return sqrt(dx * dx + dy * dy);
}

class Point3d {
  double x, y, z;
public:
  Point3d(){ x = 0; y = 0; z = 0; }
  Point3d(double x0, double y0, double z0) {
    x = x0; y = y0; z = z0;
  }
  double distance(const Point3d&) const;
  friend ostream& operator<<(ostream&, const Point3d&);
};

ostream& operator<<(ostream& output, const Point3d& p)
{
  output << "(" << p.x << ", " << p.y << ", " << p.z << ")";
  return output;
}

double Point3d::distance(const Point3d& p) const
{
  double dx = x - p.x;
  double dy = y - p.y;
  double dz = z - p.z;
  return sqrt(dx * dx + dy * dy + dz * dz);
}

int main()
{
  Point p0;
  Point p1 = Point(1, 1);
  Point3d p2;
  Point3d p3 = Point3d(1, 1, 1);
  cout << p0.distance(p1) << endl;
  cout << p2.distance(p3) << endl;
  cout << p0 << endl;
  cout << p1 << endl;
  cout << p2 << endl;
  cout << p3 << endl;
}

出力演算子を多重定義する場合、右辺式のクラスのメンバ関数として定義することはできないので、private なメンバ変数にアクセスすることはできません。public なアクセス関数を定義して、それを呼び出してもいいのですが、もうひとつ private なメンバ変数にアクセスする方法があります。それは関数を friend 宣言する、もしくは、friend の後ろで関数を定義することです。

クラスの中で関数をプロトタイプ宣言し、先頭に friend を付けると、その関数はクラスのメンバ関数と同様に private なメンバにアクセスすることができます。このプログラムでは、関数 <<(ostream&, Point&) はメンバ変数 x, y に、関数<<(ostream&, Point3d&) はメンバ変数 x, y, z にアクセスすることができます。これらの関数はメンバ変数の値をカッコに囲んで出力するだけです。簡単な処理ならば、クラスの中で関数を定義してもかまいません。

それでは実行してみましょう。

$ clang++ sample125.cpp
$ ./a.out
1.41421
1.73205
(0, 0)
(1, 1)
(0, 0, 0)
(1, 1, 1)

正常に動作していますね。

●代入演算子の多重定義

次は、代入演算子の多重定義ついて説明します。C言語の場合、構造体のメンバ変数は代入演算子 = を使ってまとめて代入することができます。クラスの場合も構造体と同じことができますが、デフォルトの動作で代入されるのはメンバ変数の値だけです。したがって、メンバ変数がポインタの場合は注意が必要です。次のリストを見てください。

リスト : インスタンスの代入 (sample126.cpp)

#include <iostream>
using namespace std;

class Foo {
  int* x;
public:
  Foo() : x(new int) {
    *x = 1;
  }
  Foo(int n) : x(new int) {
    *x = n;
  }
  // コピーコンストラクタ
  Foo(const Foo& a) : x(new int) {
    *x = *(a.x);
  }
  ~Foo() {
    delete x;
  }
  int get_x() const { return *x; }
  void put_x(int n) { *x = n; }
};

int main()
{
  Foo a;
  Foo b = Foo(10);
  a = b;
  cout << a.get_x() << endl;
  cout << b.get_x() << endl;
  a.put_x(123);
  cout << a.get_x() << endl;
  cout << b.get_x() << endl;
  b.put_x(456);
  cout << a.get_x() << endl;
  cout << b.get_x() << endl;
}

a = b; で変数 b のインスタンスを a に代入しています。このとき、b のメンバ変数 x の値が a のメンバ変数 x に代入されます。ただし、このときコピーコンストラクタは働きません。

実行結果は次のようになりました。

$ clang++ sample126.cpp
$ ./a.out
10
10
123
123
456
456
free(): double free detected in tcache 2
中止

このように、コピーコンストラクタと同様の問題が発生します。さらに、もうひとつ問題があります。最初、コンストラクタにより変数 a のメモリ領域が取得されます。ところが、a = b の代入により a のメンバ変数 x は b と同じ値に書き換えられるため、a のデストラクタが実行されても最初に取得したメモリ領域が解放されることはありません。つまり、「メモリリーク」[*1] が発生するのです。

このような場合、代入演算子 = を多重定義します。クラス X の代入演算子は次の形式で定義します。

X& operator=(const X&) {
  ...
  return *this;
}

引数が右辺(代入元)のインスタンスで、自分自身 (this) が左辺(代入先)のインスタンスです。代入先のインスタンスへの参照を返すことで、a = b = c; [*2] のようなコードを書くことができます。

プログラムは次のようになります。

リスト : 代入演算子の多重定義 (sample127.cpp)

#include <iostream>
using namespace std;

class Foo {
  int* x;
public:
  Foo() : x(new int) { *x = 1; }
  Foo(int n) : x(new int) { *x = n; }
  // コピーコンストラクタ
  Foo(const Foo& a) : x(new int) { *x = *(a.x); }
  // 代入演算子
  Foo& operator=(const Foo& a) {
    // 自分自身には代入しない
    if (this != &a) *x = *(a.x);
    return *this;
  }
  // デストラクタ
  ~Foo() { delete x; }
  // メンバ関数
  int get_x() const { return *x; }
  void put_x(int n) { *x = n; }
};

int main()
{
  Foo a;
  Foo b = Foo(10);
  a = b;
  cout << a.get_x() << endl;
  cout << b.get_x() << endl;
  a.put_x(123);
  cout << a.get_x() << endl;
  cout << b.get_x() << endl;
  b.put_x(456);
  cout << a.get_x() << endl;
  cout << b.get_x() << endl;
}

operator= の処理内容ですが、最初に自分自身の代入 (a = a;) かチェックします。これは引数 a のアドレスと this の値を比較すればいいですね。同じ場合は何もしません。そうでなければ、*x に *(a.x) の値を代入します。最初に取得したメモリ領域をそのまま使うことで、メモリリークとメモリ領域の二重解放を防ぐことができます。

それでは実行してみましょう。

$ clang++ sample127.cpp
$ ./a.out
10
10
123
10
123
456

正常に動作していますね。

-- note --------
[*1] 動的に取得したメモリは解放しないといけませんが、それを忘れて放置すると、そのメモリはプログラムが終了するまで誰にも使用することができなくなってしまいます。これをメモリリークといいます。
[*2] 代入演算子は右結合なので、a = b = c; は a = (b = c); と解釈されます。

●変換演算子

明示的な型変換 (キャスト) を行う場合、C++はC言語と同じ記法を受け継いでいます。これをCスタイルのキャストといいます。たとえば、式 e を型 T に変換するには (T)e と記述します。C++の場合、静的なキャストであれば static_cast を使いますが、このほかに関数スタイルのキャストがあります。

  1. staic_cast<T>(e)
  2. T(e)

2 が関数スタイルのキャストで、1 のキャストと同じ意味です。簡単な例を示しましょう。

リスト : 明示的な型変換 (sample128.cpp)

#include <iostream>
using namespace std;

int main()
{
  double a = 1.2345;
  cout << a << endl;
  cout << static_cast<int>(a) << endl;
  cout << int(a) << endl;
}
$ clang++ sample128.cpp
$ ./a.out
1.2345
1
1

2 つの方法で変数 b の浮動小数点数 (double) を整数 (int) に変換してから出力しています。

基本的なデータ型をクラスに変換するのは変換コンストラクタを使えば簡単にできます。逆に、クラスを基本的なデータ型 T に変換するには、変換演算子 operator T を定義します。

operator T() { ... }

返り値の型は演算子の名前と同じなので、operator の前に指定する必要はありません。

簡単な例を示しましょう。

リスト : 明示的な型変換 (sample129.cpp)

#include <iostream>
using namespace std;

class Foo {
  double x;
public:
  Foo() : x(1) { }
  Foo(double n) : x(n) { }

  operator double() { return x; }  
};

int main()
{
  Foo a(1.2345);
  cout << double(a) << endl;
  cout << static_cast<double>(a) << endl;
}
$ clang++ sample129.cpp
$ ./a.out
1.2345
1.2345

変数 a を double にキャストしてから出力しています。変換演算子が定義されていないと、型変換できずコンパイルエラーになります。

●静的メンバ変数

メンバ変数は個々のインスタンス (オブジェクト) に割り当てられる変数です。その値はインスタンスによって変わります。クラスで共通の変数や定数を使いたい場合は、class 文の中で static 変数を定義します。これを「静的メンバ変数」といいます。他のプログラミング言語では「クラス変数」と呼ばれています。簡単な例を示しましょう。

リスト : 静的メンバ変数 (sample12a.cpp)

#include <iostream>
using namespace std;

class Foo {
  int x;
public:
  Foo() { x = 1; }
  Foo(int n) { x = n; }
  int get_x() { return x; }
  void put_x(int n) { x = n; }
  // 静的メンバ変数の宣言
  static int y;
  // 静的メンバの定数は定義できる
  static const int z = 123;
};

// 静的メンバ変数の定義
int Foo::y = 2;

int main()
{
  cout << Foo::y << endl;
  Foo::y = 20;
  cout << Foo::y << endl;
  cout << Foo::z << endl;
  // Foo::z = 0;   コンパイルエラー
}
$ clang++ sample12a.cpp
$ ./a.out
2
20
123

変数 y, z は static 宣言されているので静的メンバになります。静的メンバ変数 y は宣言だけで、初期値を設定することはできません。クラスの外側でスコープ解決演算子 :: 使って int Foo::y = 2; のように実体を定義します。静的メンバの定数 z はクラスの中で定義することができます。

y と z どちらもアクセス権限は public なので、main 関数から Foo::y, Foo::z でアクセスすることができます。静的メンバ変数はインスタンスを生成しなくてもアクセスできることに注意してください。ただし、z は定数なので値を更新するコードはコンパイルエラーになります。

●静的メンバ関数

メンバ関数は個々のインスタンスを操作する関数です。一般に、ユーザが定義するメンバ関数はインスタンスを操作対象とし、クラスの動作にかかわることはありません。他のプログラミング言語では、インスタンスを操作するメソッドを「インスタンスメソッド」といいます。

これに対し、クラスの動作にかかわるメンバ関数を考えることができます。他のプログラミング言語では、これを「クラスメソッド」といいます。C++の場合は静的メンバ関数がクラスメソッドに相当します。

たとえば、クラス Foo のクラス変数 y を操作するクラスメソッド get_y(), put_y() を作りましょう。次のリストを見てください。

リスト : 静的メンバ関数 (sample12b.cpp)

#include <iostream>
using namespace std;

class Foo {
  int x;
  // 静的メンバ変数の宣言
  static int y;
public:
  Foo() { x = 1; }
  Foo(int n) { x = n; }
  int get_x() { return x; }
  void put_x(int n) { x = n; }
  // 静的メンバ関数
  static int get_y() { return y; }
  static void put_y(int n) { y = n; } 
};

// 静的メンバ変数の定義
int Foo::y = 2;

int main()
{
  cout << Foo::get_y() << endl;
  Foo::put_y(20);
  cout << Foo::get_y() << endl;
}
$ clang++ sample12b.cpp
$ ./a.out
2
20

メンバ関数 get_y と put_y は static 宣言されているので静的メンバになります。どちらの関数もアクセス権限は public なので、main 関数から Foo::get_y(), Foo::put_y() で呼び出すことができます。静的メンバ関数はインスタンスを生成しなくても呼び出すことができることに注意してください。

なお、静的メンバ関数内で this を参照することはできません。局所変数と静的メンバ変数が同じ名前の場合は、スコープ解決演算子 :: でクラス名を指定してクラス変数にアクセスしてください。

●有理数

最後に簡単な例題として、有理数 (分数) を表すクラスを作ってみましょう。有理数 (Rational) は分子と分母を整数の組で表すと簡単に定義できます。次のリストを見てください。

リスト : 有理数

#include <iostream>
using namespace std;

class Ratio {
  int n; // 分子
  int d; // 分母
public:
  Ratio() : n(0), d(1) { }
  Ratio(int x) : n(x), d(1) { }
  Ratio(int, int);
  static int gcd(int, int);
  static int abs(int x) { return x < 0 ? -x : x; }
  static int compare(const Ratio&, const Ratio&);
  friend ostream& operator<<(ostream&, const Ratio&);
  // 二項演算子
  friend Ratio operator+(const Ratio&, const Ratio&);
  friend Ratio operator-(const Ratio&, const Ratio&);
  friend Ratio operator*(const Ratio&, const Ratio&);
  friend Ratio operator/(const Ratio&, const Ratio&);
  // 単項演算子
  friend Ratio operator-(const Ratio&);
};

クラス名は Ratio としました。メンバ変数 n が分子を表し、d が分母を表します。なお、本格的な有理数クラスを作るのであれば「多倍長整数」が必要になります。今回は簡単な例題ということで、このまま作ることにします。まあ、これでも簡単なパズルであれば解くことができるでしょう。

デフォルトコンストラクタは有理数 0/1 を返します。変換コンストラクタは整数 n を有理数 n/1 に変換して返します。整数と有理数を混在させて計算するとき、整数が暗黙のうちに有理数に変換されます。2 引数のコンストラクタ Ratio(a, b) は a/b を生成して返します。次のリストを見てください。

リスト : コンストラクタ

// 最大公約数を求める
int Ratio::gcd(int a, int b)
{
  return b == 0 ? a : gcd(b, a % b);
}

// 有理数の生成
Ratio::Ratio(int x, int y)
{
  int sign = (x < 0 ? -1 : 1) * (y < 0 ? -1 : 1);
  int g = gcd(abs(x), abs(y));
  n = abs(x) / g * sign;
  d = abs(y) / g;
}

符号は分子 n に付けることにします。gcd は最大公約数を求める静的メンバ関数で、abs は絶対値を求める静的メンバ関数です。有理数を生成するとき、約分することに注意してください。符号を sign に求め、その値を分子 abs(x) / g に掛け算します。

算術演算子はコンストラクタを使うと簡単に定義することができます。次のリストを見てください。

リスト : 算術演算子の多重定義

// 二項演算子
Ratio operator+(const Ratio& a, const Ratio& b)
{
  return Ratio(a.n * b.d + b.n * a.d, a.d * b.d);
}

Ratio operator-(const Ratio& a, const Ratio& b)
{
  return Ratio(a.n * b.d - b.n * a.d, a.d * b.d);
}

Ratio operator*(const Ratio& a, const Ratio& b)
{
  return Ratio(a.n * b.n, a.d * b.d);
}

Ratio operator/(const Ratio& a, const Ratio& b)
{
  return Ratio(a.n * b.d, a.d * b.n);
}

// 単項演算子
Ratio operator-(const Ratio& a)
{
  return Ratio(-a.n, a.d);
}

どの演算子も friend 宣言しているので、メンバ変数 n, d にアクセスすることができます。あとは、単純な分数の計算なので、難しいところはないでしょう。

次は比較演算子を定義します。これは静的メンバ関数 compare を定義すると簡単です。次のリストを見てください。

リスト : 比較演算子の多重定義

int Ratio::compare(const Ratio& a, const Ratio& b)
{
  return a.n * b.d - b.n * a.d;
}

bool operator==(const Ratio& a, const Ratio& b)
{
  return Ratio::compare(a, b) == 0;
}

bool operator!=(const Ratio& a, const Ratio& b)
{
  return Ratio::compare(a, b) != 0;
}

bool operator<(const Ratio& a, const Ratio& b)
{
  return Ratio::compare(a, b) < 0;
}

bool operator>(const Ratio& a, const Ratio& b)
{
  return Ratio::compare(a, b) > 0;
}

bool operator<=(const Ratio& a, const Ratio& b)
{
  return Ratio::compare(a, b) <= 0;
}

bool operator>=(const Ratio& a, const Ratio& b)
{
  return Ratio::compare(a, b) >= 0;
}

compare は有理数 a と b を比較して、a < b ならば負の整数を返し、a == b ならば 0 を返し、a > b ならば正の整数を返します。この処理は a と b を通分して、a の分子から b の分子を引き算するだけで実現できます。あとは算術演算子にあわせて、compare の返り値と 0 を比較するだけです。

最後に出力演算子を定義します。

リスト : 出力演算子の多重定義

ostream& operator<<(ostream& output, const Ratio& rat)
{
  if (rat.d == 1)
    output << rat.n;
  else
    output << rat.n << "/" << rat.d;
  return output;
}

有理数を n/d で表示します。d が 1 ならば n のみを表示します。

それでは実際に試してみましょう。

リスト : 簡単なテスト

int main()
{
  Ratio a(1, 3);
  Ratio b(1, 7);
  cout << a << endl;
  cout << -b << endl;
  cout << a + b << endl;
  cout << a - b << endl;
  cout << a * b << endl;
  cout << a / b << endl;
  cout << b + a << endl;
  cout << b - a << endl;
  cout << b * a << endl;
  cout << b / a << endl;
  cout << a + 1 << endl;
  cout << 1 + a << endl;
  cout << (a == b) << endl;
  cout << (a != b) << endl;
  cout << (a == a) << endl;
  cout << (a != a) << endl;
  cout << (a < b) << endl;
  cout << (a <= b) << endl;
  cout << (a < a) << endl;
  cout << (a <= a) << endl;
  cout << (a > b) << endl;
  cout << (a >= b) << endl;
  cout << (a > a) << endl;
  cout << (a >= a) << endl;
}
$ clang++ ratio.cpp
$ ./a.out
1/3
-1/7
10/21
4/21
1/21
7/3
10/21
-4/21
1/21
3/7
4/3
4/3
0
1
1
0
0
0
0
1
1
1
0
1

正常に動作しているようです。興味のある方はいろいろ試してみてください。


初版 2015 年 9 月 6 日
改訂 2023 年 4 月 9 日

Copyright (C) 2015-2023 Makoto Hiroi
All rights reserved.

[ PrevPage | C++ | NextPage ]