書籍転載
文法からはじめるプログラミング言語Microsoft Visual C++入門

C++のクラスをマスターしよう(後編)
―― 第10章 クラス〜オブジェクト指向プログラミング(中編) ――

WINGSプロジェクト 矢吹 太朗(監修 山田 祥寛)
2010/06/02
Page1 Page2

10.1.12 代入演算子

 代入は初期化とは違います。前項ではコピーコンストラクタを自分で定義することによって、Aオブジェクトを別のAオブジェクトを使って初期化できるようになりました。そのコピーコンストラクタを、次のような代入で利用することはできません。

A a1;

A a2;
a2=a1; //これは代入。初期化(A a2=a1;)ではない

 代入の際には、コピーコンストラクタではなく代入演算子(operator=)が利用されます。このコードのa2=a3は、a2.operator=(a1)と同じ意味です。コピーコンストラクタと同様に、代入演算子もコンパイラが自動生成します。単純に代入すればよいフィールドのみから成るようなオブジェクトの代入には、この代入演算子で十分です。

#include <iostream>
using namespace std;

struct B
{
  int id;
};

struct A
{
  int id;
  B b;
};

int main()
{
  A a1;
  a1.id=10;
  a1.b.id=11;

  A a2; //新しいオブジェクトa2を生成し、
  a2=a1; //a1を代入する

  //代入されたことの確認
  cout<<a2.id<<endl; //出力値: 10
  cout<<a2.b.id<<endl; //出力値: 11

  //a1とa2が同じオブジェクトではないことの確認
  a1.id=20; //a1のフィールドや
  a1.b.id=22; //a1.bのフィールドを変更しても
  cout<<a2.id<<endl; //出力値: 10(a2.idは変わっていない)
  cout<<a2.b.id<<endl; //出力値: 11(a2.b.idは変わっていない)
}
[サンプル]10-assign1.cpp

 コピーコンストラクタの結果を確認したときと同じ理由で、代入演算子も正しく動作していることがわかります。

 コピーコンストラクタの場合と同じ理由で、メンバとしてポインタを持っていて、それが指すオブジェクトもコピーしたい場合には、自動生成される代入演算子は役に立ちません。そのため、代入演算子を自分で定義しなければなりません。代入演算子は次のように定義します(宣言と定義を分けることもできます)。

クラス名& operator=(const クラス名& rhs)
{
  ステートメント
}
[構文]代入演算子の定義

 代入は次のような手順で行います。

(1)idを複製する
(2)pBをtmpに保存する
(3)pBが指すオブジェクトを複製する(自動生成されたコピーコンストラクタを使う)
(4)tmpが指すオブジェクトを削除する
(5)自分自身への参照を返す

 代入演算子では、最後に自分自身への参照を返すことが大切です。これを忘れて戻り値なし(void)にしてしまうと、「a3=a2=a1」のような式をうまく処理できなくなります。この式は、「a3=(a2=a1)」のように右から評価されますが、「a2=a1」の評価結果はa2であるべきです。そうなるためには、代入演算子が自分自身への参照を返さなければならないのです。

 代入演算子を定義すると次のようになります。実行すると、「a3=a2=a1」のような場合でも正しく代入が行われていることがわかります。

#include <iostream>
using namespace std;

struct B
{
  int id;
};

struct A
{
  int id;
  B* pB;

  //Bオブジェクトを生成するデフォルトコンストラクタ
  A() : pB(new B) {}

  //コピーコンストラクタ
  A(const A& rhs) : id(rhs.id), pB(new B) { pB->id=rhs.pB->id; }

  //代入演算子
  A& operator=(const A& rhs) {
    id=rhs.id;
    B* tmp=pB;
    pB=new B(*rhs.pB); //自動生成されたコピーコンストラクタを使う
    delete tmp;
    return *this; //自分自身を返す
  }

  ~A() { delete pB; }
};

int main()
{
  A a1;
  a1.id=10;
  a1.pB->id=11;

  A a2, a3;
  a3=a2=a1; //代入

  //代入されたことの確認
  cout<<a2.id<<endl; //出力値: 10
  cout<<a2.pB->id<<endl; //出力値: 11

  cout<<a3.id<<endl; //出力値: 10
  cout<<a3.pB->id<<endl; //出力値: 11

  //a1とa2が同じオブジェクトではないことの確認
  a1.id=20; //a1のフィールドや
  a1.pB->id=22; //*a1.pBのフィールドを変更しても
  cout<<a2.id<<endl; //出力値: 10(a2.idは変わっていない)
  cout<<a2.pB->id<<endl; //出力値: 11(a2.pB->idは変わっていない)
}
[サンプル]10-assign2.cpp

 以上のように、デストラクタとコピーコンストラクタ、代入演算子を、コンパイラによって自動生成されるものの代わりに自分で実装しなければならないのは、メンバにポインタがあるときです。3つのメソッドのうちどれか1つに自分で実装する必要性を感じるときは、おそらく他の2つのメソッドも自分で実装しなければならないでしょう。

10.1.13 標準コンテナの利用

 自分で実装したクラスのオブジェクトを、コンテナ(第9章)の要素にすることができます。コンテナとしてstd::vectorやstd::listを用いる場合には、何も問題はありません。その一方で、std::setやstd::mapを用いる場合には、比較演算子「<」を定義しておかなければなりません。9.2節で述べたようにstd::setやstd::mapの要素は、常に並べ替えられた状態になっていなければなりませんが、並べ替えが可能であるためには、順番を比較できなければなりません。そして、順番の比較のためには、比較演算子「<」が必要なのです。

 次のような簡単なクラスを考えましょう。

struct Person
{
  string name;
  int age;
  Person(string name, int age) : name(name), age(age) {}
};

 次のコードのように、Personをsetの要素にすることはできません。Personのオブジェクトを比較する方法がわからないからです。

set<Person> people; //Personを要素とするset
Person taro("Taro", 32); //要素の挿入
Person hanako("Hanako", 27); //要素の挿入
cout<<(people.begin())->name<<endl; //実行時エラー(取り出し方がわからない)

 比較演算子は、左辺が右辺より小さいときにtrueが返るように定義します。たとえば、Personオブジェクトどうしを年齢で比較する演算子「<」は次のように定義できます。

bool operator<(const Person& lhs, const Person& rhs) { return lhs.age<rhs.age; }

 このように演算子を定義し直すことを、演算子のオーバーロードと言います*7。演算子をオーバーロードしておけば、Personオブジェクトをsetの要素にすることができます。

*7 演算子&& と||をオーバーロードするとショートサーキット評価(3.2.10項)が行われなくなるので注意してください。

#include <iostream>
#include <set>
#include <string>
using namespace std;

struct Person
{
  string name;
  int age;
  Person(string name, int age) : name(name), age(age) {}
  void show() { cout<<name<<" ("<<age<<")\n"; }
};

//年齢で比較する演算子
bool operator<(const Person& lhs, const Person& rhs) { return lhs.age<rhs.age; }

int main()
{
  Person taro("Taro", 32);
  Person jiro("Jiro", 50);
  Person saburo("Saburo", 100);

  set<Person> people; //Personを要素とするset

  people.insert(taro);
  people.insert(jiro);
  people.insert(saburo);

  for (set<Person>::iterator it=people.begin(); it!=people.end(); ++it) it->show();
  cout<<endl;
}
[サンプル]10-set.cpp

 実行結果は次のようになり、std::setから年齢の小さい順に取り出せていることが確認できます。

Taro (32)
Jiro (50)
Saburo (100)

 Personオブジェクトどうしを名前のアルファベット順で比較する演算子「<」は次のように定義できます(string型同士の比較は<string>に用意されています)。

bool operator<(const Person& lhs, const Person& rhs) { return lhs.name<rhs.name; }

 このように演算子を定義して、Personオブジェクトをsetの要素にして取り出すと、次のように名前のアルファベット順になっていることを確認できます。

Jiro (50)
Saburo (100)
Taro (32)

【コラム】関数オブジェクト

 メソッドとしてoperator()を持っているオブジェクトを関数オブジェクトと言います。簡単な例を以下に示します。

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

class Greeting
{
  string phrase;
public:
  Greeting(string phrase) : phrase(phrase) {}
  void operator()(const string& name) {
    cout<<phrase<<"、"<<name<<endl;
  }
};

int main()
{
  Greeting hello("コンニチハ");
  Greeting goodbye("サヨウナラ");

  hello("タロウ"); //出力値:コンニチハ、タロウ
  goodbye("ハナコ"); //出力値:サヨウナラ、ハナコ
}
[サンプル]10-func-object.cpp

 Greetingオブジェクトのメソッドoperator()は、フィールドnameの後に引数nameを追記して出力します。このように、関数オブジェクトは、オブジェクトであるにもかかわらず、関数のように使うことができます。関数オブジェクトは内部状態を持つ関数であり、さまざまな場面で応用できます。9.3.5項では、<algorithm>の関数for_each()に関数ポインタを渡す例を紹介しましたが、関数ポインタを使える場所では関数オブジェクトも使えます。

10.1.14 テンプレートクラス

 5.2.2項で紹介したテンプレート関数と同様に、クラスもテンプレートにすることができます。たとえば、第9章で紹介したvectorは、vector<int>やvector<double>のように、要素の型を指定できるテンプレートクラスです。テンプレートクラスは次のように定義します。

template <typename T>
Tを使ったクラス定義
[構文]テンプレートクラスの定義

 例として、数を2つ保持するだけの簡単なクラスを以下に示します。

#include <iostream>
using namespace std;

//テンプレートクラスの定義
template <typename T>
struct Point
{
  T x, y;
  Point(T x, T y) : x(x), y(y) {}
  T squareSum() { return x*x+y*y; } //2乗和を返す関数
};

int main()
{
  Point<int> a(3, 4);
  cout<<a.squareSum()<<endl; //出力値:25

  Point<double> b(3.0, 4.0);
  cout<<b.squareSum()<<endl; //出力値:25
}
[サンプル]10-template-class.cpp

 テンプレートクラスのオブジェクトを生成する際には、Point<int>やPoint<double>のように型を指定する必要があります。このコードで対応しているのは、int型とdouble型の2つだけですが、return文で利用している演算子「*」や「+」に対応した型ならば、どんなものでもこのテンプレートクラスPointを利用することができます。

 次回は、ここまでのクラスの機能を踏まえたうえで、オブジェクト指向プログラミングの機能を解説します。End of Article


 INDEX
  [書籍転載]文法からはじめるプログラミング言語Microsoft Visual C++入門
  C++のクラスをマスターしよう(後編)
    1.コピーコンストラクタ/暗黙の変換
  2.代入演算子/標準コンテナの利用/テンプレートクラス

インデックス・ページヘ 「文法からはじめるプログラミング言語Microsoft Visual C++入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間