연산자 오버로딩(Operator Overloading)

이재원·2024년 5월 29일
0

C++

목록 보기
2/11

연산자 오버로딩

일반적으로 사용하는 덧셈,뺄셈 등의 연산자는 기본 타입에서 사용이 가능하지만 함수나 클래스에서는 사용이 불가능하다. 이를 가능하게 하기 위해 연산자 오버로딩이라는 것을 사용한다.

연산자 오버로딩의 필요성

두 개의 정수 더하기

int a = 2, b = 3, c;
c = a + b; // 5

두 개의 문자열 합치기

string a = "C", b;
b = a + "++"; // C++

이와 같이 + 연산자를 통해 정수도 더하고, 문자열도 합쳤다. 문자열의 경우에는 헤더파일 안에 string 클래스에서 문자열을 연결해주는 + 연산자가 구현되어 있기에 위와 같은 사용이 가능한 것이다.

두 개의 객체 더하기

Color a("blue"), b("red"), c;
c = a + b; // error
a = b // error
cout << a // error

기본적으로 위 코드는 실행이 불가능하다. Color 클래스의 객체들을 더하는 방법은 어디에도 기술되어 있지 않기 때문이다.

두 피연산자를 더하기 위해 Add 함수를 정의하여 연산을 할 수 있다.

class Color {
public:
	Add(const Color& rightHand) const;
	...
};

Color a("blue"), b("red"), c;
c = a.Add(b); // 두 피연산자로 a(*this)와 b를 가짐

하지만 이와 같은 방법은 + 연산자를 사용하는 것보다 덜 직관이고, 함수의 원형을 알아야 사용할 수 있다는 단점이 있다. 이와 같은 단점을 해결하기 위해 + 연산자를 오버로딩하여 코드를 직관적으로 표현하고 가독성을 향상시킬 수 있다.

연산자 오버로딩 유의사항

1. 연산자 고유의 특성 변경 불가

본래 있는 연산자만 오버로딩 가능

C++은 +, -, *, / 등 원래 존재하는 연산자는 오버로딩하여 사용 가능하지만, 새로운 연산 처리를 추가하는 것은 불가능하다.

피연산자의 개수 변경 불가

예를 들어 이항 연산자인 + 에 대해서 피연산자가 1개 혹은 3개인 + 연산자로 오버로딩 불가능하다.

연산의 우선순위 변경 불가

연산자의 오버로딩을 통해 연산의 순위나 방향을 바꿀 수 없다.

기본값 불가

연산자 함수는 디폴트 매개 변수를 가질 수 없다.

2. 연산자 오버로딩 제약

  • 다음 연산자는 오버로딩 불가능하다.

  • =, (), [], → 연산자는 멤버 함수로만 오버로딩 가능하다.
  • 프렌드 함수로만 오버로딩 가능한 경우도 있기 때문에 기본적으로 멤버 함수로 오버로딩 하고, 안되는 경우에만 프렌드 함수로 선언해준다.

3. 함수를 통해 구현

연산자 함수를 통해 오버로딩 한다.

4. 클래스와 관계

피연산자가 적어도 하나는 객체이어야 한다. 기본 자료형에 대한 연산자 오버로딩을 방지하기 위함이다. 그러므로 연산자 함수는 클래스의 멤버 함수로 구현하든지, 아니면 전역 함수로 구현하고 클래스에 프렌드 함수로 선언한다.

5. 내장형(기본 자료형)을 포함한 연산

연산자 멤버 함수로 구현시 내장형 데이터는 반드시 두 번째 피연산자로 위치되어야 한다.

c = a + 3; // ok: Color + int
c = 3 + a; // error: int + Color

위 코드 모두 friend로 작성하면 오류 없이 사용 가능하다. friend함수로 선언하는 방법에 대해서는 나중에 별도로 설명하겠다.

연산자 함수 선언과 개요

리턴타입 operator 연산자(매개변수리스트);

연산자 함수는 이름이 operator 키워드와 연산자 로 구성된다는 점 외에는 보통 함수와 선언 방법이 동일하다.

연산자 오버로딩을 위에서 설명한대로 연산자 함수를 통해 구현하는데, 2가지의 방법이 있다.

프렌드 함수로 선언

Color operator+ (Color op1, Color op2); // 외부 전역 함수
bool operator== (Color op1, Color op2); // 외부 전역 함수

class Color {
...
	friend Color operator+ (Color op1, Color op2); // 프렌드 선언
	friend bool operator== (Color op1, Color op2); // 프렌드 선언
};
  • 연산자와 == 연산자 함수를 전역 함수로 작성하고, 두 개의 피연산자를 모두 매개 변수에 전달한다.

클래스 멤버 함수로 선언

class Color {
	...
	Color operator+ (const Color& op2) const;
	Color operator== (const Color& op2) const;
};

위 코드처럼 클래스의 멤버 연산자 함수인 경우에는 오른쪽 피연산자만 매개변수로 받는다.

연산자 함수 작성시 유의사항

매개변수는 래퍼런스로 받는다.

  • 메모리 복사가 일어나지 않아 좀 더 효율적
  • 매개변수의 복사본의 소멸시 문제 해결
    → 복사 생성자 없이 사용 가능(복사 생성자 제공하여 해결 가능하기도 함)
  • 바뀌지 않아야 하는 피연산자의 경우 const 래퍼런스로 받음

이항 연산자의 오버로딩

이항 연산자는 보통 정의된 클래스 객체를 반환한다. 이로 인해 연속적인 연산을 가능하게 하여 연산의 결과를 다른 표현식에서도 사용 가능하도록 한다. 단, 관계 연산자나 논리 연산자는 예외이다.

+ 연산자 오버로딩

우선 +연산자를 만들기 전에 +의 의미를 결정해야 한다. 이는 전적으로 개발자의 몫이다. 여기서는 + 연산을 두 Power 객체의 kick과 punch를 각각 더하는 것으로 정의한다.

Power a(3,5), b(4,6), c;
c = a + b;

이를 컴파일러는 다음과 같이 변형한다.

a . + (b);

이 식은 객체 a의 멤버 함수 operator+()를 호출하여 b를 매개 변수로 넘겨줌을 의미한다.

다음은 +연산자 함수를 구현한 것이다.

Power Power::operator+(const Power& op2) {
  Power tmp;
  tmp.kick = this->kick + op2.kick;
  tmp.punch = this->punch + op2.punch;

  return tmp;
}

== 연산자 오버로딩

비교 연산자 역시 이항 연산자이기 때문에 기본적인 작동 방식은 + 연산자와 동일하다.

코드는 다음과 같다.

bool Power::operator==(const Power& op2) {
  if(kick == op2.kick && punch == op2.punch)
	  return true;
	 else
		 return false;
}

+= 연산자 오버로딩

+= 연산자의 리턴 타입으로는 래퍼런스를 반환해 줘야 한다. 새로운 객체에 더해진 값을 할당하는 것이 아닌 피연산자 자체의 값을 바꿔야 하기 때문이다.

Power& Power::operator+=(const Power& op2) {
  kick = kick + op2.kick;
  punch = punch + op2.punch;
  return *this; // 객체 자신의 참조값 리턴
}

=(대입) 연산자 오버로딩

Power& Power::operator=(const Poser& rightHand) {
	this->kick = rightHand.kick;
	this->punch = rightHand.punch;
	
	return *this;
}

이 함수의 경우에는 새로운 객체에 값을 복사하는 것이 아닌 값 자체가 바뀌어야 하기 때문에 래퍼런스로 반환한다.

위 코드는 얕은 복사만 일어나기 때문에 힙 영역에 동적 할당된 값도 제대로 할당하고 싶다면 깊은 복사를 해줘야 한다.

class PowerList {
	Power* list;
	int size;
public:
	PowerList& operator=(const PowerList& rightHand);
	...
};

PowerList& PowerList::operator=(const PowerList& rightHand) {
	this->size = rightHand.size;
	delete[] this->list; // 기존에 할당된 메모리 해제
	this->list = new PowerList[size];
	for(int i = 0; i < this->size; i++)
		this->list[i] = rightHand.list[i];
	
	return *this;
}

단항 연산자 오버로딩

클래스 멤버 함수로 작성된 단항 연산자의 경우 하나의 피연산자(객체 자신)만을 취하므로 일반적으로 매개변수를 갖지 않는다.

만약 외부 함수로 구현한 후 friend로 선언하게 된다면 매개변수를 갖게 된다.

전위 ++ 연산자 오버로딩

Power a(3,5), b;
b = ++a;
++a = b;

전위 증감 연산은 변경된 값을 리턴하기 때문에, ++a 식은 객체 a의 모든 멤버들의 값을 1씩 증가시킨 후 변경된 객체 a의 래퍼런스로 리턴해야 한다.

위 그림과 같이 ++aa. ++ ( )로 컴파일러에 의해 변환된 후 연산자 함수를 호출하여 계산을 수행한다. ++ 연산자 함수를 구현한 코드는 다음과 같다.

Power& Power::operator++() {
	kick++;
	punch++;
	return *this;
}

프렌드로 선언할 경우

class Power {
...
public:
	friend Power& operator++(Power& op);
};

Power& operator++(Power& op) { // 참조 매개변수 사용
	op.kickk++;
	op.punch++;
	return op; // 연산 결과 리턴
}

후위 ++ 연산자 오버로딩

일반적으로 단항 연산자는 매개변수를 갖지 않지만, 전위 연산자와 구분하기 위해 매개 변수를 가진다.

Power operator++(int x);

Power Power::operator++(int x) {
	Power tmp = *this; // 증가 이전 객체 상태 저장
	kickk++;
	punch++;
	return tmp; // 증가 이전의 객체 리턴
}

후위 연산자는 기존의 값을 유지하고 해당 값만 증감 시킨다. 그렇기 때문에 증가 이전 객체를 tmp 임시 객체에 저장한 것이다. 리턴 타입은 기존의 값을 복사한 tmp를 반환하기 때문에 래퍼런스로 반환받지 않는다.

프렌드로 선언할 경우

class Power {
...
public:
	friend Power operator++(Power& op, int x);
};

Power operator++(Power& op, int x) { // 참조 매개변수 사용
	Power tmp = op; // 증가 이전 객체 상태 저장
	op.kickk++;
	op.punch++;
	return tmp; // 증가 이전의 객체 리턴
}

[ ] 연산자 오버로딩

첨자 연산자([])는 멤버 함수로만 오버로딩이 가능하다. 매개 변수는 관례적으로 int를 사용하며, 할당문의 왼쪽과 오른쪽에 모두 사용할 수 있도록 래퍼런스를 반환한다.

Power& PowerList::operator[](int i) {
	if(i < 0 || i > size - 1) {
		cout << "범위 초과\n";
		exit(-1);
	}
	
	return list[i]
}

첨자 연산자 오버로딩을 통해 객체를 정규 배열처럼 조작 가능해졌다.

-(부호) 연산자 오버로딩

Power Power::operator-() const {
	Power tmp;
	tmp.kick = -kick;
	tmp.image = -image;
	return tmp;
}

프렌드(friend)가 꼭 필요한 연산자 오버로딩

연산자 함수를 클래스 외부에 만들 경우에는 클래스에서 friend로 선언하여 클래스 멤버를 자유롭게 접근하도록 할 수 있다.

Power a(3,4), b;
b = 2 + a;

앞에서 설명한 바에 의하면 위 연산식은 2 . + (a) 로 변형하여 operator+() 함수를 호출할 것이다. 하지만 정수 2는 객체가 아니므로 컴파일러는 아래와 같은 방식으로 변형을 한다.

+ ( 2, a )

위의 식이 성공적으로 호출되기 위해서는 연산자 함수를 클래스 내부가 아닌 외부에 선언하여 구현할 수 밖에 없다.

외부에 선언된 함수이지만 클래스 멤버에 자유롭게 접근해야 연산이 수월하게 성공될 수 있으므로 friend로 선언한다.

class Power {
	int kick;
	int punch;
public:
	...
	friend Power operator+(int op1, Power op2); // friend 선언
};

Power operator+(int op1, Power op2) { // 외부 함수로 구현
	...
}

이 외에도 클래스 외부에 연산자 함수를 구현하게 된다면 클래스 내부에 friend 함수로 선언하여 사용하면 유용하게 사용할 수 있다. 다음은 friend 연산자 함수로 오버로딩이 불가능한 연산자들이다.

  • 대입 연산자(=)
  • 첨자 연산자([])
  • 호출 연산자(())
  • 멤버 접근 연산자(->)

<< 연산자 오버로딩

<< 연산자는 Shift 연산자와 출력 연산자 두 개의 의미가 있다. 이 중 출력 연산자를 오버로딩 해 보겠다.

출력 연산자 오버로딩

출력 연산자를 오버로딩 하기 위해서는 클래스 멤버 함수로 선언 가능하지만, 이는 멤버 함수로 선언하는 방법은 사용시 혼동 우려가 있어 바람직한 방법이 아니다. 따라서 friend 함수로 오버로딩을 해줘야 한다.

#include <iostream>
using std::ostream;

ostream& operator<<(ostream& os, const Power& rh) {
	os << rh.kick << ", " << rh.punch;
	return os;
}

이렇게 출력 연산자 오버로딩을 통해 객체 출력이 가능해졌다.

cout << a << "\n"; // 객체 출력 가능

위 코드를 구체적으로 설명해보면 아래와 같이 컴파일러에 의해 변형된다.

(cout.operator<<(a)).operator<<("\n");

a를 출력하고 cout을 반환하기 때문에 연속으로 출력이 가능해지는 것이다.

출처
명품 C++ Programming - 황기태
https://www.youtube.com/@sarammani

profile
20학번 새내기^^(였음..)

0개의 댓글