[따배씨++]Chapter9. 연산자 오버로딩

Date:     Updated:

카테고리:

태그:

인프런에 있는 홍정모님의 홍정모의 따라하며 배우는 C++ 강의를 듣고 정리한 내용입니다.
공부하는 식빵맘 님의 블로그를 참고했습니다.
learncpp 교재 내용을 참고했습니다.


🚆 산술 연산자 오버로딩


#include <iostream>
using namespace std;

class A
{
public:
    int data_a = 10;
}

int main()
{
    A object1;
    A object2;

    cout << object1 + object2 << endl; // error

    return 0;
}

  • C++의 경우 연산자의 오버로딩이 가능
  • 클래스간의 연산을 정의해서 사용 가능
    • add()와 같은 함수를 정의 후 return하는 방식
    • 연산자 오버로딩 하는 방식
  • 연산자간의 우선순위는 변하지 않음


(return 타입) opeator (연산자) (parameters)


class A
{
public:
    int data_a = 10;
};

int operator + (const A &a, const A &b)
{
    return a.data_a + b.data_a;
}

int main()
{
    A object1;
    A object2;

    cout << object1 + object2 << endl; // 20

    return 0;
}


오버로딩 불가능한 연산자

  • ? true:false : 3항 연산자
  • :: : namespace범위 지정 연산자
  • sizeof : 크기 연산자
  • ., .* : 멤버 선택 (포인터)연산자


🚆 전역 함수 vs 멤버 함수

전역 함수로 오버로딩


class Cents
{
private:
    int m_cents;

public:
    Cents(int cents = 0) { m_cents = cents; }

    friend int operator+(const Cents &c1, const Cents &c2); // friendly 선언
};

int operator+(const Cents &c1, const Cents &c2)
{
    return c1.m_cents + c2.m_cents;
}

int main()
{
    Cents cents1(5);
    Cents cents2(7);

    cout << cents1 + cents2 << endl; // 12

    return 0;
}

  • private 멤버에 접근해야 하는 경우 getter 함수 이용
  • 또는 friendly 함수로 접근


멤버 함수로 오버로딩


class A
{
private:
    int data_a = 10;

public:
    int operator + (const A &a)
    {
    return data_a + a.data_a;
    }

};

int main()
{
    A object1;
    A object2;

    cout << object1 + object2 << endl; // 20

    return 0;
}

  • 멤버 함수의 경우 this 파라미터가 생략되어 있음
  • priave 변수 접근 가능


🚆 입출력 연산자 오버로딩

입출력 연산에 사용되는 iostream class는 다음 두 클래스를 상속받는 클래스이다.

  • 입력을 담당하는 istream
  • 출력을 담당하는 ostream


예를 들어 std::cout은 ostream의 객체.
사실은 모두 오버로딩 해둔 것


ostream& operator<<(bool& val);
ostream& operator<<(short& val);
ostream& operator<<(unsigned short& val);
ostream& operator<<(int& val);
ostream& operator<<(unsigned int& val);
ostream& operator<<(long& val);
ostream& operator<<(unsigned long& val);
ostream& operator<<(float& val);
ostream& operator<<(double& val);
ostream& operator<<(long double& val);
ostream& operator<<(void* val);
...

  • 따라서 입출력 연산자를 멤버 함수로 선언하려면 istream/ostream 내부에서 해야 함
    • 전역함수로만 정의 가능!



class Point
{
private:
    double m_x, m_y, m_z;

public:
    Point(double x = 0.0, double y = 0.0, double z = 0.0)
        : m_x(x), m_y(y), m_z(z)
    {
    }

    friend std::ostream &operator<<(std::ostream &out, const Point &point)
    {
        out << "( " << point.m_x << ", " << point.m_y << ", " << point.m_z << " )";

        return out;
    }
};

int main()
{
    Point p1(0.0, 0.1, 0.2);
    Point p2(3.4, 1.5, 2.0);

    cout << p1 << " " << p2 << endl; // ( 0, 0.1, 0.2 ) ( 3.4, 1.5, 2 )

    return 0;
}

  • « 연산자의 오버로딩이 클래스 내부에서 정의되어 있어도 전역 함수(friend 특이케이스)
  • std::ostream 객체로 return 받아야 chaining 가능
  • ostream 객체의 « 오버로딩 하는 방식


🚆 비교 연산자 오버로딩


class Cents
{
private:
    int m_cents;

public:
    Cents(int cents = 0) { m_cents = cents; }
    int getCents() const { return m_cents; }
    int &getCents() { return m_cents; }

    friend bool operator<(const Cents &c1, const Cents &c2)
    {
        return c1.m_cents < c2.m_cents;
    }

    friend bool operator==(const Cents &c1, const Cents &c2)
    {
        return c1.m_cents == c2.m_cents;
    }

    friend std::ostream &operator<<(std::ostream &out, const Cents &cents)
    {
        out << cents.m_cents;
        return out;
    }
};

int main()
{
    vector<Cents> arr(10);
    unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();

    for (unsigned i = 0; i < 10; ++i)
        arr[i].getCents() = i;

    std::shuffle(begin(arr), end(arr), std::default_random_engine(seed));

    for (auto &e : arr)
        cout << e << " ";
    cout << endl;

    std::sort(begin(arr), end(arr));

    for (auto &e : arr)
        cout << e << " ";
    cout << endl;

    return 0;
} 

  • 비교 연산자를 오버로딩 할 경우 sorting 가능
  • <, == 연산자를 오버로딩 해야 함
  • 멤버 함수로 정의하면 정렬이 안됨 (어째서..?)


🚆 증감 연산자 오버로딩

전위 증감 연산자

class Digit
{
private:
    int m_digit;

public:
    Digit(int digit = 0) : m_digit(digit) {}

    friend ostream &operator<<(ostream &out, const Digit &d)
    {
        out << d.m_digit;
        return out;
    }

    friend Digit &operator++(Digit &digit)
    {
        ++digit.m_digit;
        return digit;
    }
};

int main()
{
    Digit d(5);

    cout << ++d << endl; // 6
    cout << d << endl;   // 6

    return 0;
}
  • 증감 연산자의 경우 자기 자신을 return godi gka
  • Parameter로는 참조 객체를 받아야 하며, 증감 연산이 이루어저야 하므로 const x


멤버 함수로 구현

Digit & operator ++ ()
{
    ++m_digit;
    
    return *this;
}


후위 증감 연산자

class Digit
{
private:
    int m_digit;

public:
    Digit(int digit = 0) : m_digit(digit) {}

    friend ostream &operator<<(ostream &out, const Digit &d)
    {
        out << d.m_digit;
        return out;
    }

    // prefix
    friend Digit &operator++(Digit &digit)
    {
        ++digit.m_digit;
        return digit;
    }

    // postfix
    friend Digit operator++(Digit &digit, int)
    {
        Digit temp(digit.m_digit);
        ++digit;
        return temp;
    }
};

int main()
{
    Digit d(5);

    cout << d++ << endl; // 5
    cout << d << endl;   // 6

    return 0;
}
  • 보통 후위 증감 연산자는 전위 증감 연산자를 통해 구현
  • 임시 객체를 만들어 저장하고, 해당 객체를 리턴 받는 방식
  • 전위 연산자와 리턴 타입만 다르고 나머지는 동일
    • 문제는 리턴 타입이 함수의 시그니쳐가 아니라는 것
    • 함수를 구분해주기 위해 dummy parameter (int) 추가


멤버 함수로 구현

Digit operator ++ (int)
{
    Digit temp(m_digit);
    ++(*this)
    return temp;
}


🚆 첨자 연산자 오버로딩

멤버 변수로 list를 갖고 있는 경우, 첨자 연산자 오버로딩이 없다면 매우 불편
포인터로 list 꺼낸 후 index로 가져와야 함

class IntList
{
private:
    int m_list[10];

public:
    int &operator[](const int index)
    {
        return m_list[index];
    }
};

int main()
{
    IntList my_list;

    my_list[3] = 10;
    cout << my_list[3] << endl; // 10

    return 0;
}
  • 주의) 참조로 리턴해야 L-Value로 사용 가능



class IntList
{
private:
    int m_list[10] = {1,2,3,4,5,6,7,8,9,10};

public:
    int &operator[](const int index)
    {
        assert(index >=0);
        assert(index<10);
        return m_list[index];
    }

    const int & operator [] (const int index) const
    {
        assert(index >=0);
        assert(index<10);
        return m_list[index];
    }
};

int main()
{
    const IntList my_list;

    my_list[3] = 10; // error
    cout << my_list[3] << endl; // 10

    return 0;
}

  • read only일 경우 const용 오버로딩 만들어 줄 수 있음
    • (const 여부로도 오버로딩 가능)
  • 첨자 연산자의 경우 assert로 범위 지정해주면 좋음


🚆 변환 생성자 - Explicit / Delete


class Fraction
{
private:
    int m_numerator;
    int m_denominator;

public:
    Fraction(int num = 0, int den = 1)
        : m_numerator(num), m_denominator(den)
    {
        assert(den != 0);
    }

    friend ostream &operator<<(ostream &out, const Fraction &f)
    {
        out << f.m_numerator << " / " << f.m_denominator << endl;
        return out;
    }
};

void doSomething(Fraction frac)
{
    cout << frac << endl;
}

int main()
{
    doSomething(7); // 7/1

    return 0;
}

  • doSomething(7)의 경우 doSomething(Fraction(7,1))로 인식 : 변환 생성자


컴파일러의 개입 없이 생성자를 명확하게 지정하고 싶은 경우


explicit Fraction(int num = 0, int den = 1)
	: m_numerator(num), m_denominator(den)
{
	assert(den != 0); 
}

int main()
{
    doSomething(7) // error

}
  • 생성자 앞에 explicit 키워드를 붙여주면 parameters가 정확히 일치해야 함



Fraction(const Fraction &frac) = delete;

  • delete : 해당 타입의 parameter는 받지 않겠다


🚆 얕은 복사 vs 깊은 복사

멤버 변수에 포인터(동적 할당 메모리) 객체가 존재하는 경우 복사 생성자 / 대입 연산자 사용에 주의해야 한다.


class MyString
{
public:
    char *m_data = nullptr;
    int m_length = 0;

    MyString(const char *source = "")
    {
        assert(source);

        m_length = strlen(source) + 1;
        m_data = new char[m_length];

        for (int i=0; i<m_length; ++i)
            m_data[i] = source[i];
        
        m_data[m_length - 1] = '\0';
    }

    // default copy constructor
    MyString(const MyString &other)
    {
        m_data = other.m_data;
        m_length = other.m_length;
    }

    ~MyString()
    {
        delete [] m_data;
    }
};

int main()
{
    MyString str1("Hello");

    MyString str2(str1); // 복사 생성자
    MyString str3 = str1; // 복사 생성자

    str2 = str1; // 대입 연산자

    return 0;
}

  • 포인터를 얕은 복사로 가져온 경우 관리가 안됨
  • 이런 경우 복사 생성자 / 대입 연산자 오버로딩 해주어야 함


복사 생성자 : 새로 메모리 할당하고 내용 복사


MyString(const MyString &source) 
{
	m_length = source.m_length; 

	if (source.m_data != nullptr) 
	{
		m_data = new char[m_length];  

		for (int i = 0; i < m_length; ++i)
			m_data[i] = source.m_data[i];
	}
	else
		m_data = nullptr;	
}


대입 연산자 : 복사 생성자와 동일 + 기존 메모리 해제 해주어야 함


MyString& operator = (const MyString & source)
{
	if (this == &source)   // 자기 자신을 대입하는 경우
		return *this;

	delete[] m_data;   // 자신의 기존 내용물 비워주기 

	m_length = source.m_length;

	if (source.m_data != nullptr)
	{
		m_data = new char[m_length];

		for (int i = 0; i < m_length; ++i)
			m_data[i] = source.m_data[i];
	}
    else
        m_data = nullptr;
}


🚆 Initializer_list

커스텀 클래스의 경우 중괄호로 편하게 초기화 하지 못함 -> initialzer_list 이용


#include <iostream>
#include <cassert>
#include <initializer_list>
using namespace std;

class IntArray
{
private:
    unsigned m_length = 0;
    int *m_data = nullptr;

public:
    IntArray(unsigned length)
        : m_length(length)
    {
        m_data = new int[length];
    }

    IntArray(const std::initializer_list<int> &list)
        : IntArray(list.size())
    {
        int count = 0;
        for (auto &element : list)
        {
            m_data[count] = element;
            ++count;
        }
    }

    ~IntArray()
    {
        delete[] this->m_data;
    }

    friend ostream &operator<<(ostream &out, IntArray arr)
    {
        for (unsigned i = 0; i < arr.m_length; ++i)
            out << arr.m_data[i] << " ";
        out << endl;

        return out;
    }
};

int main()
{
    IntArray int_array = {1, 2, 3, 4, 5}; // 중괄호로 생성자 호출 가능

    cout << int_array << endl;

    return 0;
}

  • #include
  • initialize_list의 경우 []접근 못함
  • 개별 원소를 얕은 복사로 가져오기 때문에 필요한 경우 깊은 복사로 오버로딩 해야함



맨 위로 이동하기

Cpp 카테고리 내 다른 글 보러가기

댓글 남기기