devseop08 님의 블로그

[OOP] 연산자 다중정의 본문

Language/C++

[OOP] 연산자 다중정의

devseop08 2025. 7. 21. 04:52

연산자 다중정의

연산자 다중 정의란?

  • C++의 연산자들은 C++에서 제공되는 기본 자료형에 대해서만 정의되어 있기 때문에 사용자가 정의한 클래스에 대해서는 적용할 수 없다.
  • 예를 들어 + 연산자는 int나 double 등의 자료형에 대해서는 정의되어 있지만 클래스 객체에 대해선느 정의돼있지 않다.
Complex1 c1(10, 20);
Complex1 c2(-5, 15);
Complex1 c3 = c1 + c2; // Error - Complex1을 위한 + 연산자 없음
  • 복소수라는 것은 실수를 포함하는 수의 표현으로, 이에 대해서도 double형에서와 마찬가지로 사칙연산을 포함한 여러 가지 연산이 수학적으로 정의돼 있다.
  • 그러므로 Complex1 객체에 대해서도 이러한 연산을 사용할 수 있도록 하면 보다 자연스러운 문장으로 프로그램을 작성할 수 있을 것이다.
  • 이와 같이 연산자 다중 정의는 C++에서 제공하는 연산자를 사용자가 선언한 클래스의 객체에 대하여 사용할 수 있도록 다중정의하는 것이다.
  • 연산자를 다중정의하는 구문은 함수와 같은 형식을 취하고 있으며, 임의의 문장을 사용하여 임의의 동작을 하도록 정의할 수 있다.
  • 연산자의 우선순위를 바꾼다든지, 연산방법을 바꾸는 것 등은 연산자 다중정의를 통해서는 할 수 없다. 예를 들어 이항 연산자인 && 연산자를 단항 연산자로 정의할 수 없다.
  • 의미가 비슷하다 할지라도 기존 연산자가 갖는 고유한 특성이 유지되게 하는 것도 중요.
  • 예를 들어 ++ 연산자를 정의하는 경우 ++a와 a++라는 표현의 차이를 이해하고 다중정의도 이러한 특성이 반영되도록 할 필요가 있다.
  • 연산자를 다중정의하는 위치는 클래스의 멤버로 정의할 수도 있고 별도의 일반 함수와 같이 정의할 수도 있다.
  • 기본적으로는 클래스의 멤버로 선언하는 것으로 하고 불가피한 경우는 클래스 외부의 일반 함수 형식으로 정의하게 될 것이다.

단항 연산자의 다중정의

  • 단항 연산자는 연산자가 피연산자 앞에 있는 것과 뒤에 있는 것이 있다.
  • 연산자를 피연산자 앞에 사용하는 것을 전위 표기법이라 하고, 연산자가 뒤에 위치하도록 사용하는 것을 후위 표기법이라고 한다.
  • 단항 연산자의 다중정의는 이와 같은 단항 연산자를 우리가 정의하는 자료형이나 클래스에서 사용할 수 있게 한다.
  • 단항 연산자는 전위 표기를 하였는가 후위 표기를 하였는가에 따라 그 결과가 다르므로 이를 구분하여 연산자 다중정의를 한다.

전위 표기법

  • 전위 표기 단항 연산자를 다중정의하는 구문
ReturnClass ClassName::operator opSymbol() 
{
    ...
}
// opSymbol: ++, -- 등의 단항 연산자 기호
  • 연산자를 다중정의하는 것은 함수를 정의하는 것과 크게 다르지 않다.
  • 다만 함수 이름 대신 키워드 operator와 연산자 기호를 사용하는 점이 다르다.
  • 또한 전위 표기법에 대한 단항 연산자를 정의할 때에는 인수가 없다는 점을 주의해야 한다.
  • 단항 연산자에는 피연산자가 하나(해당 객체)이므로 다른 인수를 기입하지 않는다.
class IntClass1 {
    int a;
public: 
    IntClass1(int n=0): a{n} {} // 생성자
    IntClass1& operator ++ (){ // 전위 표기 ++ 연산자 다중 정의
        ++a;        // 전위 증가에 맞게 증가 시키는 것을 제일 먼저
        return *this; // * 연산의 이유: 반환 타입을 참조 타입으로 함
    }
    int getValue() const return a;
}
  • operator ++는 객체의 상태(int a)를 변경하므로 const 키워드를 사용하지 않는다.

후위 표기법

  • 후위 표기법은 전위 표기법과 반대로 연산자를 뒤에 기입하는 방법이다.
  • 후위 표기 단항 연산자를 다중정의하는 구문
ReturnClass ClassName::operator opSymbol(int) 
{
    ...
}
// opSymbol: ++, -- 등의 단항 연산자 기호
  • 함수의 인수 기입 위치에 형식매개변수의 이름 없이 int라고 기입한 것은 int형 인수를 전달한다는 의미가 절대 아니다.
  • 이 연산자가 후위 표기법을 사용하는 단항 연산자임을 알려 주는 것이다.
  • 이것은 C++에서 사용하는 특수한 표현 방법 중 하나라고 알아두어야 한다.
class IntClass2 {
    int a;
public: 
    IntClass2(intn=0): a{n} {} // 생성자
    IntClass2 operator ++ (int) { // 후위 표기 연산자 다중정의
        // 복사 생성자의 매개변수 전달위한 * 연산
        // 새로운 객체에 후위 연산 전의 원본의 상태를 복사해둠
        IntClass2 tmp(*this)
        ++a;
        return tmp;
    }
    int getValue() const { return a; }
}
// Pencils.h
class Pencils {
    int dozens; // 타
    int np;     // 낱개
public: 
    Pencils(): dozens{0}, np{0} {};
    Pencils(int n)
        {dozens = n / 12; np = n % 12; }
    Pencils(int d, int n): dozens{d}, np{n} {}
    Pencils& operator++(); // 전위 연산자 다중정의
    Pencils operator++(int); // 후위 연산자 다중정의
    void display() const;
}
// Pencils.cpp
#include <iostream>
#include "Pencils.h"
using namespace std;

Pencils& Pencils::operator++() {
    if(++np >= 12)
        ++dozens, np = 0;
    return *this
}

Pencils& Pencils::operator++(int) {
    Pencils tmp(*this) // 현재 객체를 보존
    if(++np >= 12)
        ++dozens, np = 0;
    return tmp; // 보존된 객체를 반환
}

void Pencils::display() const 
{
    if(dozens) {
        cout << dozens << "타";
        if(np) cout << np << "자루";
        cout << endl;
    }
    else 
        cout << np << "자루" << endl;
}
// PnclMain.cpp
#include <iostream>
#include "Pencils.h"
using namespace std;

int main() {
    Pencils p1(5, 7);
    Pencils p2(23);

    p1.display();
    (++p1).display();
    p1.display();
    cout << endl;
    p2.display();
    p1 = p2++;
    p1.display();
    p2.display();
    return 0;
}
5타 7자루
5타 8자루
5타 8자루 
1타 11자루
1타 11자루 
2타

이항 산술 연산자 및 관계 연산자의 다중정의

// Complex2.h
#include <ostream>
using namespace std;
class Class2 {
    double rPart, iPart; // 실수부 및 허수부
public:
    // 생성자
    Complex2(double r=0, double i=0): rPart(r), iPart(i) {}
    Complex2 conj() const {
        return Complex1(rPart, -iPart);
    }
    Complex2 operator+(const Complex2& c) const;
    ...
};
  • 이항 연산자를 다중정의하는 구문
ReturnClass ClassName::operator opSymbol(ArgClass arg)
{
    ...
}

산술 연산자의 다중정의

덧셈 연산자의 다중정의
    1. 복소수 객체와 복소수 객체의 덧셈 연산자
      • 덧셈 연산을 수행한 후 두 피연산자의 내용이 바뀌면 안 된다. 따라서 operator+와 매개변수 c를 const로 선언하여 *this와 c의 내용이 변경되지 않도록 한 것을 볼 수 있다.
      • tmp는 덧셈 결과를 저장할 임시 객체로서 *this의 값으로 초기화되도록 복사 생성자를 사용하고 있다.
      • 임시로 사용되는 객체 대신 보다 간편하게 임시 객체를 사용하는 방법이 있다.
      Complex2 Complex2::operator+(const Complex2& c) const
      {
         return Complex2(rPart + c.rPart, iPart + c.iPart);
      }
    2. Complex2 Complex2::operator+(const Complex2& c) const { Complex2 tmp(*this); tmp.rPart += c.rPart; tmp iPart += c.iPart; return tmp; }
    1. 복소수 객체와 실수의 덧셈 연산자
    2. Complex2 Complex2::operator+(double r) const { return Complex2(rPart + r, iPart); }

클래스에 속하지 않는 연산자 다중정의

  • 연산자를 다중정의할 때 고려해야할 것은 피연산자 순서이다.
  • 연산자 다중정의는 기본적으로 교환 법칙이 성립하지 않는다.
  • a가 Complex2 클래스의 객체의 참조자일 때, a + 10.0 형태의 수식을 위한 연산자는 Complex2 클래스의 멤버로 정의할 수 있다.
  • 하지만 10.0 + a라는 수식은 사용할 수 없다.
  • a + 10.0의 operator+는 정의되어 있는 반면 10.0 + a의 operator+는 정의되어 있지 않다.
  • 좌측 피연산자인 10.0은 double 형이고 이를 Complex2 객체와 더하는 연산자는 정의되어 있지 않다.
  • 10.0 + a 형태의 수식을 사용하기 위해서는 이를 위한 연산자를 별도로 선언해야 한다.
  • 그런데 이 수식의 좌측 피연산자는 Complex2 클래스의 객체가 아니라 기본 자료형 중의 하나인 double형 값이므로이 수식을 위한 연산자는 Complex2 클래스 내가 아닌 클래스와는 별개로 외부에서 선언하고 다중정의해야 한다.
  • double + Complex2 형태의 수식을 사용하기 위한 연산자 다중정의
class Complex2 {
    ...
}

Complex2 operator+(double r, const Complex2& c) const {
    return Complex2(r+c.rPart, c.iPart);
}
  • 그런데 여기에는 문제가 있는데, 매개변수 c의 rPart 및 iPart는 c의 private 데이터 멤버이므로 Complex2 클래스 외부 영역인 operator+의 구현부에서는 c의 private 멤버인 rPart 및 iPart에 접근할 수 없다.
  • 문제를 해결할 수 있는 2가지 방법이 있는데 하나는 Complex2 클래스 내에 데이터 멤버 값을 읽을 수 있는 public 멤버함수를 정의하는 것이다.
class Complex2
    ...
public:
    ...
    double real() const { return rPart; }
    double imag() const { return iPart; }
class Complex2 {
    ...
}

Complex2 operator+(double r, const Complex2& c) const {
    return Complex2(r+c.real(), c.imag());
}
  • 다른 한 가지 방법은 friend 키워드를 이용하여 이 함수를 Complex2 클래스의 친구 함수로 지정하는 것이다.
class Complex2 {
    ...
public:
    ...
    friend Complex2 operator+(double r, const Complex2& c) const;
}

Complex2 operator+(double r, const Complex2& c) const {
    return Complex2(r+c.rPart, c.iPart);
}
  • freind 키워드를 사용하면 클래스 내부에 구현을 기입해줄 수도 있다.
class Complex2 {
    ...
public:
    ...
    friend Complex2 operator+(double r, const Complex2& c) const {
        return Complex2(r+c.rPart, c.iPart);
    }
}

+= 연산자의 다중정의

  • += 연산은 호출 객체의 값이 변경되어야 하므로 const 함수로 지정되지 않는다.
Complex2& Complex2::operator+=(const Complex2& c) {
    rPart += c.rPart; iPart += c.iPart;
    return *this;
}

관계 연산자의 다중정의

bool Complex2::operator==(const Complex2& c) const
{
    return rPart == c.rPart && iPart == c.iPart;
}

스트림 입출력 연산자의 다중정의

<< 연산자를 정의할 위치

  • 표준 출력 스트림에 정수형 변수 a에 저장된 값을 출력하려면
cout << a;
  • Complex2 클래스의 객체 c를 출력할 때에도
cout << c;
  • 이와 같이 표현하고자 할 때, << 연산자의 좌측 피연산자인 cout은 Complex2 클래스의 객체가 아니라 표준 출력 스트림을 나타내는 객체이다.
  • 따라서 << 연산자를 Complex2 클래스에서 직접 다중정의할 수 없음을 알 수 있다.
  • 이 때는 객체의 private 데이터 멤버를 반환하는 함수를 정의하든지 friend 키워드를 이용하는 방법을 사용할 수 있다.
  • << 연산자를 연쇄적으로 사용(메서드 체이닝)하기 위해선 operator<< 함수의 반환 타입은 첫 번째 매개변수 타입과 동일한 ostream&이여야만 한다.
class Complex2 {
    ...
public: 
    ...
    friend ostream& operator<<(ostream& os, const Complex2& c);
}
ostream& operator<<(ostream& os, const Complex2& c) 
{
    os << "(" << c.rPart;
    if(c.iPart > 0)
        os << "+j" << c.iPart;
    else if(c.iPart < 0)
        os << "-j" << -c.iPart;
    cout << ")";
    return os;
}

대입 및 이동 대입 연산자의 다중정의

  • 객체에 대한 기본적인 대입 연산은 객체의 데이터 멤버를 그대로 복사하는 방식
  • 만약 객체에 동적으로 할당된 메모리를 가리키는 포인터가 포함되어 있을 경우
  • 포인터의 값만 복사하므로 값을 받을 객체와 값을 제공하는 객체가 동일한 메모리를 가리키는 공유된 상태를 만들게 되어 문제를 일으킨다.
  • 이는 묵시적 복사 생성자 문제와 유사하다.
  • 이러한 유형의 클래스는 복사 생성자를 정의하는 것과 같은 이유로 대입 연산자를 정의할 필요가 있다.
  • 또한 어떠한 객체에 임시 객체를 대입할 경우에는 임시 객체의 내용을 이동함으로써 대입을 효율적으로 처리할 수 있도록 하는 것이 바람직하다.
// VecF.h
#ifndef VEC_F_H_INCLUDED
#define VEC_F_H_INCLUDED
#include <iostream>
#include <cstring>
using namespace std;

class VecF{
    int n;
    float* arr;
public: 
    VecF(int d, float* a=nullptr): n {d} { // 생성자
        arr = new floatp[d];
        if(a){
            memcpy(arr, a, sizeof(float) * n);
        }
    }

    VecF(const VecF& fv): n {fv.n} { // 복사 생성자
        arr = new float[n];
        memcpy(arr, fv.arr, sizeof(float) * n);
    }

    VecF(VecF&& fv): n {fv.n}, arr {fv.arr} { // 이동 생성자
        fv.arr = nullptr;
        fv.n = 0;
    }

    ~VecF(){
        deletep[] arr;
    }

    VecF add(const VecF& fv) const {
        VecF tmp(n);
        for(int i = 0;  i < n; i++){
            tmp.arr[i] = arr[i] + fv.arr[i];
        }
        return tmp;
    }

    void print() const {
        cout << "[ ";
        for(int i = 0; i < n; i++){
            cout << arr[i] << " ";
        }
        cout << "]";
    }
}

대입 연산자의 다중 정의

  • VecF 클래스에 대입 연산자 다중 정의
VecF& VecF::operator=(const VecF& fv)
{ 
    if(n != fv.n) // 벡터의 크기가 다르면
    {
        deletep[] arr;  // 기존 배열 메모리를 반환
        arr = new float[n=fv.n]; // 새로 메모리 할당
    }
    memcpy(arr, fv.arr, sizeof(float)*n) // 데이터 복사

    return *this
}
  • 대입 연산의 좌측 피연산자는 *this이며, 우측 피연산자는 형식 매개변수인 fv이다. fv는 VecF의 상수 참조로 선언한다.(const 참조)
  • VecF 클래스의 이동 대입 연산자 다중 정의
VecF& VecF::operator=(VecF&& fv){
    delete[] arr; 
    n = fv.n;
    arr = fv.arr;
    fv.arr = nullptr; 
    return *this; 
}
  • 이동 대입 연산의 좌측 피연산자는 *this이며, 우측 피연산자는 형식 매개변수 fv이다.
  • fv는 임시 객체를 받을 수 있도록 r-value 참조로 선언하였다.(VecF&&)
  • fv의 내용을 *this에 얕은 복사를 한 후 fv의 arr에 nullptr을 대입함으로써 fv의 내용이 *this에 이동되도록 하였다.
int main() 
{
    float a[3] = {1, 2, 3};
    float b[3] = {2, 4, 6};
    VecF v1(3, a);
    VecF v2(3, b);
    VecF v3(3);
    VecF v4(v1); // 복사 생정자
    VecF v5(v1.add(v2)) // 이동 생성자
    v3 = v1;  // 대입 연산자
    cout << v3 << endl;
    v3 = v1.add(v2); // 이동 대입 연산자
}
  • 두 변수의 값을 교환해야 하는 경우가 많이 있는데, 이 경우 이동 대입을 활용하면 효율을 높일 수 있다.
  • 대입하는 대상이 l-value 참조이면 복사 생성자 혹은 대입 연산자가 선택되어 사용되는데, 이 경엔 새로운 메모리를 할당하고 값을 복사하게 된다.
  • 복사해야 하는 요소가 많아질수록 비효율적이다.
void swapVecF(VecF& v1, VecF& v2) 
{
    VecF tmp = v1; // 복사 생성자 사용
    v1 = v2; // 대입 연산자 사용
    v2 = tmp // 대입 연산자 사용
}
  • 이러한 대입에 이동 대입 연산자를 사용할 수 있다면 효율을 크게 높일 수 있는데 이를 위해 표준 라이브러리의 move 함수를 이용할 수 있다.
  • move 함수는 인수로 전달되는 객체의 r-value 참조를 반환하는 함수이다.
void swapVecF(VecF& v1, VecF& v2){
    VecF tmp = move(v1); // 이동 생성자 사용
    v1 = move(v2); // 이동 대입 연산자 사용
    v2 = move(tmp); // 이동 대입 연산자 사용
}

[] 연산자의 다중정의

  • [] 연산자는 배열의 첨자를 저장하는 연산자이다. [] 연산자를 다중 정의하면 보다 명료한 프로그램을 작성할 수 있다.
class SafeIntArray {
    int limit;      // 원소의 개수
    int* arr;       // 데이터 저장공간
public:
    SafeIntArray(int n): limit(n) {
        arr = new int[n]; // 공간 할당
    }
    ~SafeIntArray() {
        delete[] arr;
    }
    int size() const { return limit; }
    // i번째 원소를 반환하는 멤버함수
    int& operator[](int i) {     // 반환 타입을 l-value 참조로
        if(i < 0 || i >= limit) {
            std::cout << "첨자가 범위를 벗어나 프로그램을 종료합니다.";
            exit(EXIT_FAILURE);
        }
        return arr[i]; // i번째 원소 값 반환
    }
    int operator[](int i) const { // const 함수 선언: const 객체에서 호출 가능
        if(i < 0 || i >= limit) 
            std::cout << "첨자가 범위를 벗어나 프로그램을 종료합니다.";
            exit(EXIT_FAILURE);
        }
        return arr[i]; // i번째 원소 값 반환
    }
} 
int main() 
{
    SafeIntArray a[10];

    for(int i = 0; i < 10; i++)
        a[i] = i;   // int& operator[](int i) 시그니처의 함수 호출
    cout << a[5] << endl;    // int operator[](int i) const 시그니처의 함수 호출
}
  • SafeIntArray 클래스에 정의된 [] 연산자는 2개이다.
  • 반환 타입이 l-value 참조 타입 int&로 선언된 [] 연산자는 [] 연산자로 SafeIntArray 객체 안의 배열 요소에 값을 대입할 수 있게 해준다.
  • const로 선언된 [] 연산자는 const로 선언된 SafeIntArray 객체에 대해 [] 연산자로 객체 안 배열 요소의 값을 읽을 수 있게 해준다.
  • [] 연산자가 const로 선언되지 않았다면 다음과 같은 함수는 컴파일 에러가 발생한다.
void f(const SafeIntArray& arr)
{
    for(int i = 0; i < arr.size(); i++)
        cout << arr[i] << endl; // Error 
}

자료형의 변환 연산 다중정의

  • 서로 다른 클래스의 객체들 사이에서도 묵시적 형 변환이 일어나도록 할 수 있다.
class Meter{
    int m; // 미터
    int cm; // 센티미터
public: 
    // 생성자
    Meter(): m(0), cm(0) {}
    Meter(int meter, int cmeter): m(meter), cm(cmeter){}
    void display() const {
        if(m)
            cout << m << "m";
        if(cm || !m)
            cout << cm << "cm";
        cout <<endl;
    }
    int getM() const { return m;}
    int getCm() const { return cm; } 
}
class Feet{
    int ft; // 피트
    int in; // 인치
public: 
    Feet(): ft(0), in(0) {}
    Feet(int f, int i): ft(f), in(i) {}
    Feet(const Meter &m)  // Meter -> Feet 형 변환 연산자
    {
        int cmeter = m.getM() * 100 + m.getCm();
        in = static_cast<int>(cmeter/2.54 + 0.5);
        ft = in / 12;
        in %= 12;
    }
    operator Meter() const { // Feet -> Meter 형 변환 연산자
        int m = static_cast<int>((ft * 12 + in) * 2.54 + 0.5);
        return Meter(m / 100, m % 100);
    }
    void display() const {
        if(ft)
            cout << ft << "m";
        if(in || !in)
            cout << in << "cm";
        cout <<endl;
    }
}
int main() 
{
    Meter mLen;
    Feet fLen(10, 5);
    mLen = fLen; // operator Meter() const 연산자 호출
    fLen = mLen; // Feet(const Meter &m) 연산자 호출
}

연산자 다중정의의 주의 사항

  • 연산자 중에는 다중정의를 할 수 없는 것도 있다.
    • 멤버 선택 연산자(.)
    • 멤버에 대한 포인터 연산자(*)
    • 유효범위 결정 연산자(: :)
    • 조건 연산자(?:)

'Language > C++' 카테고리의 다른 글

[API] STL  (3) 2025.07.31
[OOP] 제네릭과 템플릿  (4) 2025.07.31
[OOP] 상속과 다형성  (1) 2025.07.16
[Basic] 함수와 스코프  (0) 2025.07.16
[Basic] 연산자와 흐름 제어 구문  (6) 2025.07.16