본문 바로가기
Language/C++

12.연산자 오버로딩

by 아무키 2023. 12. 25.
반응형

 연산자 오버로딩 (Operator Overloading)

 

+, = , *등등의 연산자를 유저 정의 타입(ex. 클래스)에 대해 사용할 수 있도록하는 기능

→ player1 + player2

유저 정의 타입을 기본 타입(int, float, etc)과 유사하게 동작하게 할 수 있다.

코드를 보다 이해하기 쉽고, 작성하기 쉽게 만듬

→ 항상 좋은 것은 아니다. 직관성과 가독성을 높인다고 생각이 들 경우 활용

대입 연산자(=)를 제외하면 자동 생성되지 않으며, 사용자가 구현해 주어야 함

#include <iostream>

class Player{
};

int main(){
	Player p1;
	Player p2;

	Player p3 = p1 + p2;

	int a = 1, b = 2;
	int c = a + b;
}

 

예) 숫자 하나를 갖는 number 클래스를 작성하고, 클래스 객체끼리의 사칙연산 기능이 필요하다고 가정

만일 (a + b) * (c / d)를 계산해야한다면

 

(전역)함수를 사용하여 구현

Number result = multiply(add(a, b), divide(c, d));

 

멤버 함수를 사용해 구현

Number result = (a.add(b)).multiply(c.divide(d));

 

연산자 오버로딩

Number result = (a + b) * (c / d);

 

연산자 오버로딩, 예제 클래스

class Point{
	private:
		int xpos, ypos;
	public:
		Point(int x = 0; int y = 0)
				:xpos{x}, ypos{y}{}
		void ShowPosition() const{
			cout << xpos << ", " << ypos << endl;
		}
};

int main(){
		Point p1{2, 3};
		p1.Showposition();
}// 2, 3 출력

 

연산자 오버로딩, 기본 규칙

연산의 우선순위를 바꿀 수는 없음(*는 + 보다 먼저 계산)

단항, 이항, 삼항 연산의 교체는 불가능

기본 타입(int, float, etc)에 대한 연산자는 오버로딩 불가능

새로운 연산자의 정의 불가

연산자는 멤버 함수 또는 전역 함수로 오버로딩 가능

단, [ ], ( ), → , = 등 몇몇 연산자는 멤버 함수로만 오버로딩 가능

 

연산자 오버로딩, 사용 목적

Point는 기본 자료형이 아닌 사용자 정의 클래스

연산자 오버로딩을 통해 연산자를 통한 객체의 동작 정의

Point p1{10, 20};
Point p2{30, 40};

Point p3 = p1 + p2; 
// 현재는 불가능하지만
//연산자 오버로딩을 하면 가능해짐

cout << (p1 < p2) << endl;
cout << (p1 == p2) << endl;

cout << p3 << endl;

 

 

멤버 함수인 연산자 오버로딩 (Operator Overloading as Member Function)

 

이항(binary) 연산자의 멤버 함수로의 선언(+, -, ==, ≠ , >, <, etc)

 

선언 형태

Point operator+(const Point& rhs) const;
Point operator-(const Point& rhs) const;
bool operator==(const Point& rhs) const;
bool operator<(const Poiont& rhs) const;

 

사용 예시

Point p1{10, 20};
Point p2{30, 40};
Point p3 = p1 + p2; // p1.operator + (p2);
p3 = p1 - p2; //p1.operator + (p2);

if(p1 == p2) // p1.operator == (p2)
	...

 

+연산자의 오버로딩 동작 방식의 이해

Point operator+(const Point& rhs) const;

Point p3 = p1 + p2; //p1.operator+(p2);
//p1에 operatr+를 찾아 p2를 넘겨줌

 

+연산자의 오버로딩 구현

Point operator+(const Point& rhs) const
{
	return Point{xpos + rhs.xops, ypos + rhs.ypos};
}
Point p3 = p1 + p2; //p1.operatr+(p2);

 

비교 연산자의 오버로딩

bool operator==(const Point& rhs) const{
//p2은 변하지 않으므로 const Point& rhs
//p1도 변하지 않으므로 const{
	if(xpos == rhs.xops && ypos == rhs.ypos)
	{
		return true;
	}
	else{
		return false;
	}
}

 

 

단항(Unary) 연산자의 멤버 함수로의 선언(++, —, -, !)

 

선언 형태

Point operator-() const;
Point operator++();
Point operator++(int) //post_increment
bool operator!() const;

 

사용 예시

Point p1{10, 20};
Point p2 = -p1; //p1.operator-();
p2 = ++p1; //p1.operator++();
p2 = p1++; //p1.operator++(int);

 

-연산자 오버로딩

Point operator-() const{
	return Point{-xpos, -ypos};
}

 

 

멤버 함수로 선언시 한계점(Limitation of Member Function)

 

교환 법칙이 성립하도록 구현이 불가능할 수 있다.

예를 들어 자료형이 다른 경우, 3, p1

→ p1 * 3은 함수 호출 가능, 3 * p1은 함수 호출 불가

Point p1{1, 20};
Point p2{30, 40};

Point p3 = p1 * 3; //p1.operator*(3)
p3.ShowPosition();

//Point p4 = 3 * p1; // 3.operator*(p1) ???

 

전역 함수인 연산자 오버로딩

 

이항(binary) 연산자의 전역 함수로의 선언(+, -, ==, ≠, >, <, etc)

operator 오버로딩을 전역 함수로 선언(point::operator가 아님)

lhs도 매개변수로 전달

→ 이러한 구현을 위해서는 함수를 friend로 선언하는 것이 일반적

Point operator+(const Poinst& lhs, const Point& rhs);
Point operator-(const Points& lhs, const Point& rhs);
bool operator==(const Point& lhs, const Point& rhs);
bool operator<(const Point& lhs, const Point& rhs);

Point p1{10, 20}; 
Point p2{30, 40};
Point p3 = p1 + p2; //operator+(p1, p2);
p3 = p1 - p2;

if(p1 == p2) // operator == (p1, p2)

 

+연산자의 오버로딩

friend Point operator+(const Point& lhs, const Point& rhs){
	return Point(lhs.xpos + rhs.xpos, lhs.ypos + rhs.ypos);
}

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

//operator+ 함수는 Point 클래스의 멤버 함수가 아니기 떄문
//xpos ypos 쉽게 접근하기 위해 friend 선언

 

Friend 유의사항

class Point{
	private:
		int xpos;
		int ypos;
	public:
		Point(int x, int y)
			: xpos{x}, ypos{y}{}
	friend Point operator+(const Point& lhs, const Point& rhs){
		return Point{lhs.xpos + rhs.xpos, lhs.ypos +yrhs.ypos};
	}
};

//opertator+ 함수가 class Point{}안에 있다고해서 멤버 함수가 아니다.
//아래의 코드를 줄여쓴 것일 뿐이다.
//operator+는 전역 함수이다.

class Point{
	private:
		int xpos, ypos;
	public:
		Point(int x, int y)
			: xpos{x}, ypos{y}{}
	friend Point operator+(const Point& lhs, const Point& rhs);
};

Point operator+(const Poiont& lhs, const Point& rhs){
	return Point{lhs.xps + rhs.xpos, lhs.ypos + rhs.ypos};
}

 

*연산자의 구현

→ 3*p1도 가능하도록 하기 위해 전역 함수를 추가 구현

마찬가지로, 함수의 friend를 가정

→ Friend가 아닌 경우 lhs.xpos, rhs.xpos 접근 불가

Point operator*(int scale)const{
	return Point{xpos * scale, ypos * scale
};
friend Point operator*(int scale, const Point& rhs){
	return rhs*scale;
}

 

 

스트림 삽입 및 추출 연산자 오버로딩

(Stream Insertion and Extraction Operators Overloading)

 

구현하고자 하는 예시

Point p1{10, 20};
Point p2{30, 40};

p1.ShowPosition(); // [10, 20]
p2.ShowPotision(); // [30, 40] //복잡한 멤버 함수 대신

cout << p1 << endl; //[10, 20] // 기본 자료형처럼 stream 출력 가능하게

Point p3;
cin >> p3; // 50 60 //기본 자료형처럼 stream 입력 가능하게

 

cout에 대한 간단한 이해

<<은 cout 객체(ostream 클래스)의 오버로딩된 연산자이다.

class MyOstream{
	public:
		void operator<<(int val){
			printf("%d", val);
		}
};

int main(){
		cout << 123; //cout << operator<<(123);
		cout.operator << (123);
		MyOstream mycout; // 위에서 만든 클래스 객체(mycout)을 사용한 콘솔 출력
		mycout << 123;
		mycout.operator << (123);
		return 0;

 

스트림 삽입 연산자 오버로딩, 구현

ostream의 참조자를 반환하여 chain insertion(ex << AA << BB등 연속적으로)이 가능하도록 구현해야 함

→ 참조자를 반환하지 않으면 cout << p1 << p2와 같은 연산 불가

→ 또한 기본적으로 cout 객체는 복사가 불가(참조자 & 사용)

friend ostream& operator<<(ostream& os, const Point& rhs){
	os << '[' << rhs.xpos <<", " << rhs.ypos << ']';

	return os;
}

//std::cout << p1; // cout.operator<<(p1) <== x
		// operator<<(cout, p1) <== o 를 희망

 

전역 함수로 선언 필요

→ 멤버 함수로 선언하면 아래와 같이 사용

p1 << cout; //p1.operator<<(std::cout);

 

스트림 추출 연산자 오버로딩, 구현

우측 매개변수(rhs)는 const가 아님에 유의

friend istream& operator>>(istream& is, Point& rhs){
	int x{}, y{};
	is >> x >> y;
	rhs = Point{x, y};
	return is;
}

std::cin >> p1; //operator>>(cin, p1);

전역 함수로 선언

→ 삽입 연산자와 마찬가지

 

대입 연산자 오버로딩

C++는 대입 연산자(=)를 자동 생성

자동 생성된 대입 연산자는 얕은 복사를 수행(복사 생성자 생각남)

Point p1{10, 20};
Point p2{30, 40};

Point p3 = p1; // 대입 연산이 아닌 복사 생성
p3.ShowPosition();

Point p4;
p4 = p1; // 대입 연산
p4.ShowPotision();

 

대입 연산자 오버로딩, 선언 (Assignment Operator Overloading)

기본 패턴

Type& operator=(const Type& rhs);

참조형으로 반환하여 추가적인 복사 연산을 하지 않도록 함

 

사용 예

Point& operator=(const Point& rhs)

p4 = p1 // 대입연산하면 아래 operator=함수 호출
p4.operator=(p1);
#include <iostream>

class Point{
	private:
		int xpos, ypos;
	public:
		Point(int x, int y)
			: xPos{x}, yPos{y}{}
		Point()
			: Point{0, 0}{}
		Point& operator=(const Point& rhs){
			xPos = rhs.xPos;
			yPos = rhs.yPos;
		}
};

int main(){
	Point p1{10, 20};
	Point p2{30, 40};

	Point p3
	p3 = p1; // p3.operator=(p1)  <- 컴파일러가 자동 생성
					 // 위에서 임의로 만들어줌
					 // operaotr=(p3, p1)

 

대입 연산자 오버로딩 구현

Point& operator=(const Point& rhs){
	if(this = &rhs) //왼쪽 객체의 주소(this)가 오른쪽 객체의 주소와
		return *this; //같은 경우 (p1 = p1) 왼쪽 객체의 주소를 역참조하여 반환
	xpos = rhs.xpos;
	ypos = rhs.ypos;

	return *this;
}

 

대입 연산자 오버로딩, 깊은 복사

C++는 대입 연산자도 자동 생성

자동 생성된 대입 연산자는 얕은 복사를 수행

포인터 타입의 멤버 변수가 존재하면, 깊은 복사를 통한 대입 연산자 직접 정의 필요

 

복사 생성자 내용 복습

→ 복사 생성자는 자동 생성됨

→ 자동 생성된 복사 생성자는 얕은 복사를 수행

포인터 타입의 멤버 변수가 존재하는 경우, 깊은 복사의 직접 정의가 필요

class Array{
	private:
		int* ptr;
		int size;
	public:
		Array(int val, int size)
			: size(size){
			ptr = new int[size];
			for(int i = 0; i < size; i++){
				ptr[i] = val+ i;
			}
		}
		int GetSize() const{
			return size;
		}
		int GetValue(int index) const{
			if(index < size && index >= 0)
				return ptr[index];
		}
		~Array(){
				delete[] ptr;
		}
};

 

대입 연산자 오버로딩, 깊은 복사

대입 연산자의 구현

Array& operator=(const Array& rhs){
	if(this == &rhs)
		return *this;
	//안하면 a = a의 경우 쓰레기값 저장

	delete[] ptr;

	ptr = new int[rhs.size];

	size = rhs.size;

	for(int i = 0 ; i < size; i++){
		ptr[i] = rhs.ptr[i];
	}
	return *this;
}

//데이터를 배열로 가지고 있을 경우 해제후 재할당이 필요
//데이터의 길이가 변할 수 있다.

 

참고 상속에서의 대입 연산자 오버로딩

class Base{
	int value;
	
	public:
		...
		Base& operator=(const Base& rhs){
			if(this != &rhs){
				value = rhs.value;
			}
			return *this;
	}
};


class Derived : public Base{
	int double_balue;
	
	public:
		...
		Derived& operator=(const Derived& rhs){
			if(this != &rhs){
				Base::operator=(rhs);
				
				double_value = rhs.double_value;
			}
		return *this;
	}
};

// 기본 클래스에 대한 대입 연산자를 호출하고 난 뒤,
//유도 클래스의 속성에 대한 대입 연산을 처리하도록 구현

//만일 기본 클래스 연산자를 호출하지 않는다면, value 값에
//대한 대입 연산은 수행하지 않음

 

참고) 상속에서의 복사 / 이동 생성자와 오버로딩된 대입 연산자

유도 클래스에서 사용자가 이를 구현하지 않은 경우,

→ 컴파일러가 자동으로 생성하며, 기본 클래스를 위한 복사 / 이동 생성자를 호출

유도 클래스에서 사용자가 이를 구현한 경우

→ 기본 클래스를 위한 복사/이동 생성자를 사용자가 반드시 호출해 주어야함

 

따라서, 포인터형 멤버 변수를 가지고 있는 경우, 기본 클래스의 복사 / 이동 생성자를 호출하는 방법에 대해 반드시 숙지해 두어야 함

→ 유도 클래스 멤버 변수에 대한 깊은 복사 고려

 

첨자 연산자 오버로딩 (Subscript [ ] operator overloading)

 

[ ] 연산자

멤버 함수로 오버로딩 필요

→[ ] ,( ), →, = 와 같은 몇몇 연산자는 멤버 함수로만 오버로딩 가능

경계 검사 등 기능 확장을 위해 용이하게 사용

 

첨자 연산자 오버로딩

첨자 연산자 오버로딩 예제를 위한 클래스 정의

Point의 동적 할당된 배열과 그 크기를 멤버 함수로 갖는 PointArr 클래스 정의

class PointArr{
	private:
		Point* arr;
		int arr_len;
	public:
		PointArr(int len)
				:arr_len{len}
		{
				arr = new Point[len];
		}
		int get_arr_len() const{return arr_len;}
		~PointArr() {delete[]arr;}
};

 

참조 연산자 오버로딩의 구현

Point& operator[](int idx){ //참조형 반환 이유는?
	if(idx < 0 || idx >= arr_len){
			cout << "Array out of Bound!" << endl;
			exit(1);
	}
	return arr[idx];
}

//cout << a1[0]; <= a1.operator[](0)

 

참조형으로 반환해야만 arr[0] = Point{10, 20}과 같이 배열 내부 데이터에 접근 가능

→ 참조형으로 반환하지 않으면 복사된 값이 반환된다는 것을 상기

Point operator[](int idx) const{ //const 오버로딩 하는 이유는?
	if(idx < 0 || idx >= arr_len){
			cout << "Array out of Bound!" << endl;
			exit(1);
	}
	return arr[idx];
}

//cout << a1[0]; <= a1.operator[](0)

 

const PointArr 사용에 있어서 데이터에 접근 가능해야 하기 때문

 

첨자 연산자 오버로딩에서 고려해야 할 점

첨자 연산자 오버로딩에서 배열에 대한 복사 생성 허용 여부의 결정이 필요

배열에 대한 복사 생성과 대입을 차단하고 싶을 경우

private:
	Point* arr;
	int arr_len;

	PointArr(const PointArr &arr){} //priavate 복사 생성자
	PointArr& operator=(const PointArr &arr){} //private 대입 연산자

/// 또는 (아래 최신버전)

private:
	Point* arr;
	int arr_len;

public:
	PointArr(const PointArr &arr) = delete;
	//delete는 해당 함수를 구현하지 않는다는 의미
	PointArr& operator = (const PointArr &arr) = delete;

 

요약

연산자 오버로딩 개요

   클래스에 대한 연산자의 적용 방식을 사용자가 직접 오버로딩하여 구현할 수 있다.

멤버 함수인 연산자 오버로딩

   클래스의 멤버함수로 operatorX()라는 이름을 갖는 함수를 구현하여 해당 클래스에 대한 연산자를 오버로딩할 수 있다. 이때 다른 피연산자는 인자로 넘어온다.

전역 함수인 연산자 오버로딩

   멤버함수로 구현시 교환법칙 문제가 발생할 수 있고, 이러한 경우 전역 함수로 오버로딩하여 friend 키워드르 사용하면 편리함

 

스트림 삽입 및 추출 연산자 오버로딩

   <<, >>도 연산자이며, cout/cin 객체에 대해 오버라이딩 하면 됨. Chain insertion을 위한 참조자 반환

대입 연산자 오버로딩

   기본 대입 연산자는 얕은 복사를 수행하기 때문에 깊은 복사가 필요한 경우 대입 연산자를 직접 오버로딩이 반드시 필요

첨자 연산자 오버로딩 ex) a1[0]

   일반적으로 const 멤버와 참조 반환 함수 두개를 오버로딩하여 구현

반응형

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

14.STL  (1) 2023.12.25
13.제네릭 프로그래밍  (0) 2023.12.25
11.다형성  (1) 2023.12.25
10.상속  (0) 2023.12.23
09.this-const-static-friend  (1) 2023.12.22

댓글