[C++] 명품 C++ Programming 7장 : 프렌드와 연산자 중복

녹차·2024년 6월 22일
0

C++

목록 보기
1/11

C++ 프렌드 개념

친구와 C++ 프렌드

사람들은 누구나 Friend가 있다. 친구가 우리 집에 오면 알아서 냉장고에서 음료수를 마시고 TV도 켜서 본다. 우리 식구랑 같이 밥도 먹고, 졸리면 내 침대에서 자기도 한다. 내 어머니를 어미니라고 부르면 애교를 떨기도 한다. 내가 초대한 친구들 가족들은 가족의 한 사람으로 대한다. 친구란 내 가족의 일원은 아니지만 내 가족의 동일한 권한을 가진 일원으로 인정받은 사람이다.

갑자기 친구 얘기를 왜 하나 하는 생각이 들 것이다. C++의 friend 키워드에 대한 설명을 하기 위해서다. C++ 에서는 friend 키워드에 대한 설명을 하기 위해서이다. C++에서는 클래스 외부에 작성된 함수를 클래스 내에 friend 키워드로 선언하여, 클래스의 멤버 함수와 동일한 접근 자격을 부여할 수 있다. 물론 멤버가 아니므로 상속되지 않는다. 클래스 내에 friend 키워드로 선언된 외부 함수를 프렌드 함수라고 부르며 프렌드 함수는 마치 클래스의 멤버인 것처럼 클래스의 모든 변수나 함수에 접근할 수 있다.

프렌드 함수는 왜 필요할까? 프렌드 함수를 클래스의 멤버 함수로 작성하면 되지 않을까? 프로그램을 작성하다보면, 클래스 멤버 함수로는 적합하지 않지만, 클래스의 private, protected 멤버를 접근해야 하는 특별한 경우, 이 함수를 외부 함수로 작성하고 프렌드로 선언한다. 프렌드 함수가 가장 유용하게 사용되는 대표적인 경우는 연산자 함숭디ㅏ.

  • 클래스 외부에 작성된 함수를 프렌드로 선언

  • 다른 클래스의 멤버 함수를 프렌드로 선언

  • 다른 클래스의 모든 멤버 함수를 프렌드로 선언

프렌드 함수 선언

클래스 외부에 작성된 함수를 클래스 내에 프렌드로 선언하는 방법을 알아보자. 클래스 외부에 구현된 함수를 friend 키워드로 클래스 내의 아무 곳에나 선언하면 된다. 다음은 클래스 외부에 작성된 equals() 함수를 클래스 Rect에 friend 함수로 선언하는 사례이다.

class Rect{
....
	friend bool equals(Rect r, Rect s)
};

equals()는 Rect 클래스의 모든 멤버를 자유롭게 접근할 수 있다.

잠깐! - forward reference 문제 해결을 위한 forward declaration

C++에서는 다음과 같이 변수나, 함수, 클래스의 이름을 먼저 선언한 후 그 이름을 참조하는 backward reference가 원칙이다.

class Rect {
...
};
int main() {
	Rect rect; //backward reference
}

하지만 가끔 뒤에서 선언되는 이름을 미리 참조(사용)하는 경우가 발생한다. 이런 경우 forward reference라고 부른다. 컴파일 입장에서는 아직 선언되지 않은 이름을 참조(사용)하므로 forward reference를 컴파일 오류로 처리한다.

class Rect;

bool equals(Rect r, Rect s); //선언된 Rect 사용

class Rect {
	friend bool equals(Rect r, Rect s);
};

class Rect; 선언으로 forward reference 문제 해결

프렌드 멤버 선언

다른 클래스의 멤버 함수를 클래스의 프렌드 함수로 선언할 수 있다. 다음은 RectManager 클래스의 멤버 함수 equals(Rect r, Rect s)를 Rect 클래스에 프렌드로 선언하는 사례이다.

class Rect {
	....
    friend bool RectManager::equals(Rect r, Rect s) 
    //RectManager의 equals() 멤버 함수를 프렌드로 초대
};

프렌드 클래스 선언

다른 클래스의 모든 멤버 함수를 클래스의 프렌드 함수로 한번에 선언할 수 있다. 다음은 코드는 RectManager 클래스를 Rect 클래스에 초대한다.

class Rect{
	....
    friend RectManager; //
};

이렇게 함으로써 RectManager의 모든 멤버 함수는 Rect 클래스의 모든 멤버를 자유롭게 접근할 수 있다.

[예제 7-3]
#include<iostream>

using namespace std;

class Rect;

class RectManager
{
public:
	bool equals(Rect r, Rect s);
	bool copy(Rect& dest, Rect& src);
};

class Rect
{
	int w, h;
public:
	Rect(int w, int h) {
		this->w = w;
		this->h = h;
	}
	friend RectManager; //RectManager 클래스의 모든 함수를 friend함수로 선언
};

bool RectManager::equals(Rect r, Rect s)
{
	
}

bool RectManager::copy(Rect& dest, Rect& src)
{

}

int main()
{
	Rect a(3, 4), b(5, 6);
	RectManager man;

	man.copy(b, a);
	man.equals(a, b);
}

연산자 중복

연산자 중복의 개념

+, -, x, / 등 수학 연산자는 사칙 연산뿐 아니라 사물이나 색깔, 행위 등 생활 속에서도 자주 사용되고 있다.

  • 기호로 표현된 숫자 더하기, 색 혼합, 결혼은 모두 다른 행위이지만 사람들은 + 의미를 알아서 이해한다. 이것은 동일한 연산 혹은 기호(+)를 대상(피연산자)에 따라서로 다른 의미로 해석하는 일종의 다형성(polymorhism)이다.
    C++언어에서 연산자 중복도 이와 다르지 않다. 같은 이름의 함수를 여러 개 만들 수 있는 것이 function overloading이라면, 피연산자에 따라 서로 다른 연산을 하도록 동일한 연산자를 중복해서 작성하는 것이 operator overloading이다. 피연산자에 적합한 연산자를 새로 작성하면 프로그램이 보다 쉅게 표현된다.

두 개의 정수 더하기

C++에서 기본적으로 정의된 +연산자는 다음과 같이 숫자 더하기만 한다.

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

두 개의 문자열 합치기

그러나 다음과 같이 + 연산자는 문자열을 연결하는 표현으로 사용할 수 있다.

string a = "C", c;
c = a + "++"

실제 < string> 헤더파일에는 string 클래스와 함께 문자열을 연결하는 + 연산자가 구현되어 있다.

두 색을 섞어 새로운 색 만들기

하나의 색을 표현하는 Color 클래스가 있다고 할때 다음과 같이 + 연산자로 두 색을 혼합하여 새로운 색을 만드는 것을 표현할 수 있다.

Color a(BLUE) b(RED), c;
c = a + b;

물론 이를 위한 +연산자가 만들어져야 한다.

두 개의 배열 더하기

정렬된 배열을 표현하는 클래스 SortedArray가 있다고 할 때, 두 개의 배열을 합쳐서 새로운 배열을 만들 때, + 연산자를 이용하여 다음과 같이 표현할 수 있다.

SortedArray a(2, 6, 9), b(3, 7, 10), c;
c = a + b; //c = {2, 3, 6, 7, 9, 10};

이 또한, 배열을 합치는 + 연산자가 만들어져야 한다. 이처럼 연산자가 중복은 코드를 직관적으로 표현할 수 있게 함으로써, 프로그램 가독성을 높여주는 긍정적인 기능을 한다.

연산자 중복의 특징

C++언어에 본래 있는 연산자만 중복 가능하다.

연산자 중복은 C++ 언어에 본래부터 있는 연산자에 새로운 연산 처리를 추가하는 것이다. +, - , /, ==, !=, %, && 등 본래부터 있는 C++ 연산자에 새로운 의미를 부여하는 것은 가능하지만 %%, ## 등 새로운 연산자를 만들어 낼 수 없다.

연산자 중복은 피연산자의 타입이 다른 연산을 새로 정의하는 것이다.

C++에서 기본 + 연산자의 피연산자는 모두 숫자이다. 그러므로 + 연산자를 새로 중복하려면 '객체 + 수', '수 + 객체', '객체 + 객체'와 같이 정수나 실수가 아닌 객체나 값을 더하는 + 연산이어야 한다.

연산자 중복은 함수를 통해 이루어진다.

연산자 중복이란 새로운 연산 처리를 수행하는 함수를 구현하는 것이다. 이 함수를 연산자 함수라고 부른다.

연산자 중복은 반드시 클래스와 관계를 가진다.

중복된 연산자는 반드시 피연산자에 객체를 동반하다. 그러므로 연산자 함수는 클래스의 멤버 함수로 구현하든지, 아니면 전역 함수로 구현하고 클래스에 프렌드 함수로 선언한다.

연산자 중복으로 피연산자의 개수를 바꿀 수 없다.

예를 들어 이항 연산자인 +에 대해, 피연산자가 1개 혹은 3개인 + 연산자로 중복할 수 없다.

연산자 중복으로 연산의 우선 순위를 바꿀 수 없다.

연산자의 중복을 통해 연산의 순위나 방향을 바꿀 수 없다. 예를 들어 '2 + 5 6'의 경우 연산자의 우선순위가 +보다 높기 때문에 먼저 계산되어 결과가 32가 된다. 이런 연산의 순위를 바꿀 수 있는 연산자 중복은 불가능하다.

연산자 함수 선언과 연산자 함수 개요

연산자 중복은 연산자 함수를 통해 구현된다. 연산자함수는 다음의 2가지 방법으로 작성 가능하다.

  • 클래스의 멤버 함수로 구현
  • 외부 함수로 구현하고 클래스의 프렌드 함수로 선언

연산자 함수를 선언하는 방법을 알아보자 operator 키워드와 함께 다음과 같이 연산자 함수를 구선언한다.

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

연산자 함수는 이름이 'operaotr' 키워드와 '연산자'로 구성된다는 점 이외에는 보통 함수 선언과 동일하다. 연산자 함수를 클래스의 멤버 함수로 구현하느냐 아니면 외부 함수로 구현하고 프렌드로 선언하느냐에 따라 연산자 함수의 매개 변수 리스트는 달라진다. 두 Color 객체를 더하여 Color 객체를 리턴하는 + 연산자 함수와 두 Color 객체를 비교하여 true, false를 리턴하는 == 연산자의 예를 들어 비교해보자.

외부 함수로 구현하고 클래스에 프렌드 함수로 선언하는 경우

+연산자와 == 연산자 함수는 다음과 같이 외부 전역 함수로 작성하고, 두 개의 피연산자를 모두 매개 변수에 전달한다.

Color operaor +(Color op1, Color op2); //외부 전역 함수
bool operaor ==(Color op1, Color op2); //외부 전역 함수
...
class Color{
...
	friend Color operator +(Color op1, Color op2);
    friend bool operator ==(Color op1, Color op2);
};

클래스의 멤버 함수로 선언되는 경우

+연산자와 == 연산자 함수를 Color의 멤버 함수로 구현할 때 다음과 같이 선언한다.

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

+나 ==의 오른쪽 피연산자만 매개변수 op2에 전달되고, 왼쪽 피연산자는 객체 자신이므로 매개 변수에 전달되지 않는다.

클래스의 멤버로 구현되었든 외부 함수로 구현되었든 +, ==연산자는 다음과 같이 사용된다.

Color a(BLUE), b(RED), c;
c = a + b;

if(a == b)
{
}

잠깐! operator+()와 operator + ()

operator+()를 operator + ()로 띄어 써도 무관하다

이항 연산자 중복

이 절에서 연산자 함수를 클래스의 멤버 함수로 작성하며, 외부 함수로 작성하는 방법은 이후에 다룬다. 피연산자가 2개인 이항 연산자(binary operator)를 중복해보자. 이 과정을 알고 나면 전체적으로 연산자 함수를 선언하고 구현하는 방법을 알게 된다. 지금부터 Power라는 클래스를 가지고 여러 가지 이항 연산자의 중복을 설명한다. Power는 게임에서 인물이나 기계의 에너지(파워)를 표현하는 것으로, 발로 차는 kick과 주먹으로 때리는 힘 punch의 두 멤버 변수로 모델링한다.

+연산자 중복

연산자 착안

두 개의 Power 객체를 더하는 + 연산자를 만들기 전에 우선 +의 의미를 결정해야 한다.

  • 연산의 의미를 정하는 것은 전적으로 개발자 의 몫이다. 여기서는 + 연산을 두 Power 객체의 kick과 punch를 각각 더하는 것을 정의한다.
    이제, 다음과 같이 3개의 Power 객체 a, b, c를 생성하고,
Power a(3,5), b(4, 6), c;

a, b를 합치는 +연산자는 다음과 같이 사용한다.

c = a + b;

연산자 함수 선언

C++의 기본 더하기(+) 연산에서는 피연산자에 수 이외의 값을 올 수 없기 때문에, 컴파일러는 a + b의 연산이 C++의 기본 더하기로 처리될 수 없음을 판단한다. 그리고 Power 클래스에 Power 객체를 더하는 + 연산자 함수가 새로 선언되어 있는지 찾는다. 이를 위해 컴파일러는 a + b 식을 다음과 같이 변형한다.

a . + ( b );

Power 객체 a의 멤버 변수 함수 operaotr+() 를 호출하며, b 매개 변수로 넘겨주는 함수 호출이다. 우리는 이 호출이 성공할 수 있도록 다음과 같이 operator+() 함수를 Power 클래스의 멤버 함수를 Power 클래스의 멤버 함수로 선언한다.

class Power {
	Power operator+ (Power op2);
};

operator+() 함수는 리턴 타입을 Power로 하고, 더한 결과로 새로운 Power 객체를 리턴한다.

연산자 함수 구현

+연산자와 Power 객체의 kick 멤버와 punch 멤버를 각각 더하는 것이므로, 다음과 같이 연산자 함수로 구현한다.

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

여기서, this는 Power 객체 a 자신에 대한 포인터이며 op2는 Power 객체 b를 전달받은 매개 변수이므로, this->kick + op2.kick;은 a의 kick과 b의 kick을 더하는 것이다. 이 연산자 함수는 더한 결과 tmp 객체를 리턴한다

[예제 7-4]
#include<iostream>

using namespace std;

class Power
{
	int kick;
	int punch;
public:
	Power(int kick = 0, int punch = 0)
	{
		this->kick = kick;
		this->punch = punch;
	};
	void show();
	Power operator+ (Power op2); // +연산자 함수 선언
};

void Power::show()
{
	cout << "kick=" << kick << ',' << "punch=" << punch << endl;
}

Power Power::operator+(Power op2)
{
	Power tmp; //임시 객체 생성
	tmp.kick = this->kick + op2.kick;
	tmp.punch = this->punch + op2.punch;

	return tmp;
}

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

	a.show();
	b.show();
	c.show();
}

단항 연산자 중복

피연산자가 하나인 단항 연산자(unary operator)의 연산자 함수를 클래스의 멤버 함수로 작성하는 방법을 소개한다. 단항 연산자는 연산자의 위치에 따라 전위 연산자(prefix operator)와 후위 연산자(postfix operator)로 나눤다. 전위 연산자는 연산자가 피연산자 앞에 오는 경우이며, 후위 연산자는 연산자가 뒤에 오는 경우이다. 다음의 전위 연산자와 후위 연산자의 사례를 보여 주며, op는 피연산자이다.

  • 전위 연산자 : !op, ~op, ++op, --op
  • 후위 연산자 : op++, op--

단항 연산자 !와 ~는 전위 연산자로만 사용되지만, ++와 -- 연산자는 전위나 후위 연산자 모두 사용된다.

전위 ++ 연산자 중복

먼저, 전위 ++ 연산자의 사례로 전위 연산자를 중복하는 방법에 대해 알아보자.

연산자 착안

Power 객체 a, b에 대해 전위 ++연산자는 다음과 같이 사용 가능하다.

Power a(3, 5), b;
b = ++a;
++a = b; //++a는 I-value가 될 수 있음

++a 식은 객체 a의 모든 멤버(kick과 punch)들의 값을 1씩 증가시킨 후, 변경된 객체 a의 참조를 리턴하는 것으로 정의한다.

연산자 함수 선언

컴파일러는 ++a 식을 다음과 같이 변형하여 Power 클래스에 선언된 operaotr++() 연산자 함수를 호출한다.

a . ++ ();

이 호출이 성공하도록 하기 위해, 매개 변수 없는 operator++() 연산자 함수를 선언한다. operator++() 함수의 리턴 타입은 Power& 이다.

참조를 리턴하는 연산자 함수 구현

operator++() 연산자 함수는 다음과 같이 자신의 kick과 punch를 각각 1씩 증가시킨 후, 자신(*this)에 대한 참조를 리턴한다.

[예제 7-8]
#include<iostream>

using namespace std;


class Power
{
	int kick;
	int punch;
public:
	Power(int kick = 0, int punch = 0)
	{
		this->kick = kick;
		this->punch = punch;
	};
	void show();
	Power& operator++();
};

void Power::show() {
	cout << "kick= " << kick << ',' << "punch=" << punch << endl;
}

Power& Power::operator++()
{
	kick++;
	punch++;

	return *this;
}

int main()
{
	Power a(3, 5), b;
	a.show();
	b.show();
	
	b = ++a;

	a.show();
	b.show();
}

프렌드를 이용한 연산자 중복

지금까지 연산자 함수르 모두 클래스의 멤버 함수로 작성하였다. 연산자 함수는 클래스 바깥의 외부 전역 함수로도 작성 가능하다. 이런 경우, 연산자 함수를 클래스에서 friend로 취하여 클래스의 멤버를 자유롭게 접근할 수 있다. 이제부터 연산자 함수를 클래스의 외부 함수로 작성하고 프렌드로 선언하는 방법을 알아보자.

2 + a를 위한 +연산자 함수 작성

+연산자를 외부 함수로 작성

다음 + 연산 식을 보자.

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

지금까지 배운 바에 따르면, 컴파일러는 2 + a 연산을 다음과 같이 변형하여 operator+() 함수를 호출하려고 한다.

2. + ( a )

그러나 2가 객체가 아니므로 이 변형식은 완전히 잘못된 문장이다. 이처럼 첫 번째 피연산자가 객체가 아닌 경우, 컴파일러는 다음과 같이 변환한다.

+ ( 2 + a)

사실 컴파일러에게 두 개의 옵션이 있는 셈이다. 앞의 식이 성공적이기 위해서는 operator+() 함수를 Power 클래스의 외부 함수로 밖에 구현할 수 없다. +연산자를 Power 클래스의 외부 함수로 구현한 코드를 보여준다. 이처럼 어떤 연산의 경우, 연산자 함수를 오직 외부 함수로만 작성해야 하는 경우가 있다.

외부 연산자 함수의 프렌드 선언

operator+(int, Power) 연산자 함수에는 한 가지 걱정거리가 있다. 함수 내에서 Power의 private 멤버인 kick과 punch를 자유롭게 접근하고 있기 때문이다. 이대로라면, 이 연산자 함수에 컴파일 오류가 발생할 것이다 뻔하다. 이 문제의 손쉬운 해결책은 kick과 punch를 public으로 선언하면 된다. 그러나 이러한 해결책은 연산자 함수를 작성하고자 Power 클래스의 캡슐화 원칙을 무너뜨리게 되는 치명적인 판단 미스라고 볼 수 있다. friend를 사용하면 깔끔하게 해결된다. 외부에 구현된 operator+(int, Power)의 연산자 함수를 다음과 같이 Power 클래스에 프렌드로 초대하는 방법이다.

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

Power operator+(int op1, Power op2) { //외부 함수로 구현된 연산자 함수

}
[예제7-11]
#include<iostream>

using namespace std;

class Power
{
	int kick;
	int punch;
public:
	Power(int kick = 0, int punch = 0)
	{
		this->kick = kick; 
		this->punch = punch;
	};
	void show();
	friend Power operator +(int op1, Power op2);
};

void Power::show() {
	cout << "kick= " << kick << ',' << "punch=" << punch << endl;
}

Power operator +(int op1, Power op2)
{
	Power tmp;
	tmp.kick = op1 + op2.kick; //kick 더하기
	tmp.punch = op1 + op2.punch; //punch 더하기

	return tmp;
}

int main()
{
	Power a(3, 5), b;
	a.show();
	b.show();

	b = 2 + a;

	a.show();
	b.show();
}

참조를 리턴하는 << 연산자 작성 실습

초급자에게 참조 리턴은 어렵다. 반복하다 보면 감을 잡게 된 날이 온다. <<연산자를 만들어보면서 참조 리턴을 이해해보자. Power 객체 a에 대해 다음 << 연산자를 구현해보라.

Power a(1, 2);
a << 3 << 5 << 6;

연산자 착안

이 연산은 객체 a의 kick과 punch에 각각 3을 더하고 다시 5, 6을 연속적으로 더하는 연산이다. 실행 결가 객체 a의 kick은 15, punch은 16이 된다.

연산자 함수 구현

여기서 잠깐, << 연산이 어떻게 진행되어야 하는지 생각해보자. 'a << 3'의 실행 후 다음 '<<5' 연산이 객체 a에 대해 진행되려면, 'a<<3'에서 연산자 <<가 연산 후 객체 a의 참조를 리턴해야 한다. 컴파일러는 a<<3 연산을 다음과 같이 변형한다.

a. << ( 3 );

우리는 이에 맞추어 <<연산자에 함수를 다음과 같이 Power 클래스의 멤버로 선언한다.

#include<iostream>

class Power
{
public:
	Power& operator << (int n); //연산 후 Power 객체의 참조 리턴
};

Power& Power::operator<<(int n)
{
	kick += n;
	punch += n;

	return *this;
}

만일 참조를 리턴하지 않고 다음과 같이 구현한다면

Power operator << (int n); //참조를 리턴하지 않으면 연속된 << 연산이 정상 작동하지 않음

a<<3 연산에서 <<연산자가 객체 a의 복사본을 리턴하기 때문에 'a<<3' 후에 계속되는 '<<5' 연산은 객체 a의 복사본에 5를 증가시키는 결과가 된다. <<연산자가 제대로 작동하려면 << 연산자 함수가 원본에 대한 참조를 리턴해야 한다.

[예제 7-14]
#include<iostream>

using namespace std;

class Power
{
	int kick;
	int punch;
public:
	Power(int kick = 0, int punch = 0)
	{
		this->kick = kick;
		this->punch = punch;
	};
	void show();
	Power& operator <<(int n);
};

void Power::show() {
	cout << "kick= " << kick << ',' << "punch=" << punch << endl;
}

Power& Power:: operator <<(int n)
{
	kick += n;
	punch += n;

	return *this;
}

int main()
{
	Power a(1, 2);

	a << 3 << 5 << 6;

	a.show();
}

실행결과
kick=15, punch=16
profile
CK23 Game programmer

0개의 댓글