C++ 発展編4日目

引数の型や数を変えた関数のオーバーロードについては基礎編 4日目で説明をしました
ここでは、演算子のオーバーロードについて説明をします

演算子のオーバーロード

演算子のオーバーロードとは、新しいクラスを作成したときに、そのクラスと演算子の動作を関連付けることで、他のデータ型と同じように演算を扱えるようにした機能のことです

例えば、std::stringでは、文字列の連結は以下のように記述しています

std::string greeting = "Hello";
std::string name = "World";
std::string message = greeting + ", " + name + "!"; // ここに注目!

+演算子は本来、数値の足し算のために存在するものです
そのため、上記のコードではコンパイルエラーがでるはずです
しかし、std::stringには「+演算子が、文字列の連結に使われたらこういう処理をする」という定義(オーバーロード)がされています
それにより、数値を足し算するのと同じ感覚で、直感的に文字列を連結できるのです

ほかにも、以下のようなオーバーロードが定義されています

std::string str1 = "apple";
std::string str2 = "banana";
if (str1 == str2) { // 文字列の比較
    // ...
}

ここでの==演算子も、数値の比較ではなく、文字列の比較として機能するようにオーバーロードされています

std::string text = "C++";
text += " is fun!"; // 末尾に文字列を追加

+=演算子も同様に、文字列の末尾に別の文字列を追加する意味でオーバーロードされています
このように、std::stringの例を見ると、演算子のオーバーロードは「既存の演算子に、クラス固有の新しい意味や動作を与える仕組み」であることが分かります

演算子オーバーロードを定義する場合はoperatorキーワードとオーバーロードする演算子の記号という組み合わせで記述します
std::stringでは、+=+===<<>>などが定義されています
詳細はstd::basic_string – cppreference.comを参照ください

次からは、自作のクラスに演算子のオーバーロードを定義する方法について説明をしていきます
オーバーロードできる演算子など詳細については、下記のURLを参照してください
演算子オーバーロード – cppreference.com

二項演算子のオーバーロード

二項演算子とは、2つのオペランド(値や変数)に対して演算を行う演算子のことです
例えば、加算の「+」や減算の「-」、乗算の「*」、割り算の「/」など、基本的な演算が二項演算子に該当します
二項演算子をオーバーロードする際には、メンバ関数として記述する方法と、非メンバ関数として記述する方法とがあります

  1. メンバ関数としてオーバーロードする場合
戻り値の型 operator演算子(const&rhs);

引数とオブジェクト自身の演算を行い、結果を返します

#include <iostream>

class Point{
    double x;
    double y;

public:
    Point(double x1,double y1): x(x1),y(y1) {}
    Point operator+(const Point &rhs) ; // +演算子のオーバーロード
    Point& operator+=(const Point &rhs) ; // +=演算子のオーバーロード

    double getX() const {return x;}
    double getY() const {return y;}

    void show(){
        std::cout << "x=" << x << std::endl;
        std::cout << "y=" << y << std::endl;
    }
};

// +演算子のオーバーロードの関数の定義
// 自分自身と引数のメンバをそれぞれ加算し、加算した結果を新しいPointクラスを戻り値とする
Point Point::operator+(const Point &rhs){
    return Point(this->x + rhs.x,this->y + rhs.y);
}

// +=演算子のオーバーロードの関数の定義
// 自分自身と引数のメンバをそれぞれ加算し、加算した結果になった自分自身の参照を戻り値とする
Point& Point::operator+=(const Point &rhs){
    this->x += rhs.x;
    this->y += rhs.y;
    return *this;
}

int main(){
    Point p1(1.5,1.5);
    Point p2(2.5,3.5);

    Point p3 = p1 + p2;  // p1.operator+(p2) が呼び出される
    p3.show();

    p1 += p2;  // p1.operator+=(p2) が呼び出される
    p1.show();

    return 0;
}

複合代入演算子(+=, -=, *= など)は、オブジェクトの状態を変更するため、メンバ関数としてオーバーロードします

  1. 非メンバ関数(グローバル関数またはフレンド関数)としてオーバーロードする場合
 戻り値の型 operator演算子(const type& lhs, const type& rhs);

二つの引数の演算を行い、結果を戻り値として返します

#include <iostream>

class Point{
    double x;
    double y;

public:
    Point(double x1,double y1): x(x1),y(y1) {}

    double getX() const {return x;}
    double getY() const {return y;}

    void show(){
        std::cout << "x=" << x << std::endl;
        std::cout << "y=" << y << std::endl;
    }
};

// Pointクラス同士の+演算子を非メンバ関数としてオーバーロードした関数
Point operator+(const Point &lhs,const Point &rhs) {
    return Point(lhs.getX() + rhs.getX(),lhs.getY() + rhs.getY());
}
// Pointクラスと値を引数として+演算子を非メンバ関数としてオーバーロードした関数
Point operator+(const Point &lhs, double value) {
    return Point(lhs.getX() + value, lhs.getY() + value);
}
// 値とPointクラスを引数として+演算子を非メンバ関数としてオーバーロードした関数
Point operator+(double value, const Point &rhs) {
    return Point(value + rhs.getX(), value + rhs.getY());
}

int main(){
    Point p1(1.5,1.5);
    Point p2(2.5,3.5);

    Point p3 = p1 + p2;  // operator+(p1,p2)
    p3.show();

    Point p4 = p1 + 0.5; // operator+(p1,0.5)
    p4.show();

    Point p5 = 0.5 + p2; // operator+(0.5,p2)
    p5.show();

    return 0;
}

左右のオペランドの型に柔軟性を持たせたい場合(例: Point + double や double + Point のような異なる型の組み合わせ)や、オペランドの順序に依存しないような対称性を保ちたい場合は、二項演算子(+, -, * など)で新しいオブジェクトを返す非メンバ関数としてオーバーロードします

単項演算子のオーバーロード

単項演算子とは、単一のオペランド(対象となる変数や値)に対して演算を行う演算子です
例えば、単項マイナス(-)、インクリメント演算子(++)、デクリメント演算子(–)、否定演算子(!)、ビット単位の反転(~)などが単項演算子です
これらの演算子は、単独で与えられたオペランドに作用して結果を返します

単項演算子のオーバーロードも、メンバ関数と非メンバ関数で実装することができます

  1. メンバ関数として記述する方法
戻り値の型 operator演算子();

引数は不要です

以下は、単項演算子の-をオーバーロードし、メンバx,yの符号を反転しています

#include <iostream>

class Point{
    double x;
    double y;

public:
    Point(double x1,double y1): x(x1),y(y1) {}
    Point operator-() const; // 単項演算子の宣言

    double getX() const {return x;}
    double getY() const {return y;}

    void show(){
        std::cout << "x=" << x << std::endl;
        std::cout << "y=" << y << std::endl;
    }
};

Point Point::operator-() const{
    return Point(-this->x,-this->y);
}

int main(){
    Point p1(10.5,2.5);
    p1 = -p1;
    p1.show();

    return 0;
}

Point クラスに、単項のマイナス演算子(符号反転)をオーバーロードすることで、Point オブジェクトのX座標とY座標の両方の符号を反転させる機能を追加しています
戻り値は、新しいPointオブジェクトです

  1. 非メンバ関数として記述する方法
戻り値の型 operator演算子( 引数の型 );

演算する引数のクラス型を渡します

#include <iostream>

class Point{
    double x;
    double y;

public:
    Point(double x1,double y1): x(x1),y(y1) {}

    double getX() const {return x;}
    double getY() const {return y;}

    void show(){
        std::cout << "x=" << x << std::endl;
        std::cout << "y=" << y << std::endl;
    }
};

// 非メンバ関数としてー演算子のオーバーロード関数を定義
Point operator-(const Point& p){
    return Point(-p.getX(),-p.getY());
}    

int main(){
    Point p1(10.5,2.5);
    p1 = -p1;
    p1.show();

    return 0;
}

1のメンバ関数として記述する方法で示したサンプルプログラムを非メンバ関数で書き換えました

Point operator-(const Point& p)

引数 pのx,yの値を反転した新しいオブジェクトを作成して戻り値としています

※単項演算子は、通常そのクラスのオブジェクトに対して適用されるため、メンバ関数としてオーバーロードするのが一般的です

インクリメント演算子、デクリメント演算子

インクリメント、デクリメントには前置と後置の2種類が存在します

  • 前置
    ++a、–a : 加算、減算した結果の値を返します
  • 後置
    a++、a– : 加算、減算する前の値を返します

インクリメント演算子、デクリメント演算子をオーバーロードする場合、上記の異なる動作をそれぞれ対応しなければなりません
そのため、インクリメント演算子、デクリメント演算子の後置形式の場合、前置形式と区別するため、int (通常は値 0) を渡す決まりがあります

以下に、インクリメント演算子、デクリメント演算子の前置、後置の合計4通りのサンプルプログラムを示します

#include <iostream>

class Point{
    int x;
    int y;

public:
    Point(int x1,int y1): x(x1),y(y1) {}
    Point& operator++() ; //前置インクリメント
    Point& operator--() ; //前置デクリメント

    Point operator++(int); //後置インクリメント
    Point operator--(int); //後置デクリメント

    int getX() const {return x;}
    int getY() const {return y;}

    void show(){
        std::cout << "x=" << x << std::endl;
        std::cout << "y=" << y << std::endl;
    }
};

Point& Point::operator++(){ //前置インクリメント
    x++;
    y++;
    return *this; // 自分自身の参照を戻り値とする
}

Point& Point::operator--(){ //前置デクリメント
    x--;
    y--;
    return *this; // 自分自身の参照を戻り値とする
}

Point Point::operator++(int){ //後置インクリメント
    // 後置は変更前の値を返すので、自分自身のコピーを作成する
    auto tmp = *this;
    ++x;
    ++y;
    return tmp;
}

Point Point::operator--(int){ //後置デクリメント
   // 後置は変更前の値を返すので、自分自身のコピーを作成する
    auto tmp = *this;
    --x;
    --y;
    return tmp;
}

int main(){
    Point p1(10,20);

    ++p1;
    std::cout << "前置インクリメント" << std::endl;
    p1.show();

    --p1;
    std::cout << "前置デクリメント" << std::endl;
    p1.show();

    Point p2 = p1++; // p2に代入後インクリメントする
    std::cout << "後置インクリメント" << std::endl;
    p2.show(); // インクリメント前の値が表示される
    p1.show(); // インクリメントした値が表示される

    Point p3 = p1--; // p3に代入後デクリメントする
    std::cout << "後置デクリメント" << std::endl;
    p3.show(); // デクリメント前の値が表示される
    p1.show(); // デクリメントした値が表示される
    return 0;
}

※インクリメント、デクリメント演算子のオーバーロードも単項演算子同様にメンバ関数としてオーバーロードするのが一般的です

コメント

この記事へのコメントはありません。

関連記事

C++ 4日目:電卓アプリの仕様の確認

Java(応用編)演習問題

Java 応用編 2日目

PAGE TOP