.. Cover Letter

ㅇ 공부#언어/(C++)기초

(기초 C++) 7장. C++에서의 프랜드와 연산자 중복

BrainKimDu 2023. 3. 1. 15:43

주의 : 이 글은 C의 기초문법에 대해 상세하게 다루지 않습니다. (즉 C언어에서 배울 수 있는 기초내용은 생략합니다)


프랜드 (Friend)

클래스내에서 선언한 멤버함수들이 있을텐데, 클래스 외부에서 선언한 멤버함수를 클래스 내부에 있는 것처럼 만들고 싶다면 사용하는게 바로 프랜드이다. 그래서 프랜드 함수는 클래스내의 모든 변수나 함수에 접근할 수 있다.

프랜드가 왜 필요한지에 대해 한 번 다시 생각을 해보면, 클래스에는 3가지의 접근권한이 있다. public protected private, 만약 클래스 외부 함수에서 protected, private 멤버변수를 접근해야하는 상황이라 하면, 절대로 접근할 방법이 없다. 그럴 때 사용하는게 프랜드라고 생각하면 된다. 

그러면 이제 프랜드 함수를 한 번 선언해보자. 가장 간단한 예시를 들고오면

#include <iostream>
using namespace std;

//class animal; 이거 있어야 컴파일 에러가 나지 않음.
void smae_age(animal a1, animal a2);

class animal {
	string name;
	int age;
public:
	animal(string name) {
		this->name = name;
		this->age = 0;
	}

	friend void same_age(animal a1, animal a2);
};


void same_age(animal a1, animal a2) {
	if (a1.age == a2.age)
		cout << "둘을 친구네요.";

	else
		cout << "둘은 나이가 다르네요";
}


int main() {
	animal a1("dog"); animal a2("cat");
	same_age(a1, a2);
}

프랜드 함수를 클래스내에서 선언해주고, 밖에서 함수를 구현한다. 원래 클래스 메소드 였다면 
animal::same_age 와 같이 구현을 했었다.  그러나 프랜드 함수이기 때문에 외부에서 작성을 해도된다.

여튼 이 코드를 실행하면 

다음과 같은 컴파일 에러가 나온다. 방법은 두 가지 맨위에서 저 코드를 지우거나 class animal;을 추가해주면된다.

여튼 여기서 발행하는 오류를 backward reference 라고 하는데, 앞에서 선언된 함수가 뒤에서 나올 변수를 사용하려 하기 때문에 오류가 발행한다. 그렇기 때문에 전방참조 (위의 경우처럼 해주어야한다.)

이는 C언어에서도 처음에 등장하는 개념으로 코드가 길어질 것을 우려해서 함수를 맨밑에서 선언할때, 맨위에 이함수가 있어요라고 미리알려주는 것과 비슷한 개념으로 생각하면된다. 

 

다음은 프렌드 멤버 선언을 알아보자. 다른 클래스의 멤버 함수를 클래스의 프렌드 함수로 선언할 수 있다.  다음을 보자.

#include <iostream>
using namespace std;

class animal;

class hospital {
	int price;
public:
	hospital(){
		this->price = 1000;
	}
	void get_history(animal a1);
};

class animal {
	string name;
	int age;
public:
	animal(string name) {
		this->name = name;
		this->age = 0;
	}
	friend void hospital::get_history(animal a1);
};

void hospital::get_history(animal a1) {
	cout << a1.name << " " << a1.age << "살 진료비는 " << price << "원 입니다.";
}

int main() {
	animal a1("dog");
	hospital doc;
	doc.get_history(a1);
}

위에서 부터 차근차근 내려오면서 알아보면 병원 클래스의 get_history 함수를 선언을 했다. 매개변수로는 animal 클래스의 객체를 넣어주는 상황이다.

animal 클래스를 보면 hospital의 get_history 멤버함수(메소드)를 프렌드로 초대했다. 그래서 이 함수를 통해 hospital의 멤버변수에 접근할 수 있는 상태이다. 일단 프랜드로 지정하고 클래스 외부에서 함수를 구현한다.

외부의 함수를 살펴보면 a1의 객체를 받아온 상태이다. 원래 a1.name을 바로 호출하면 안된다. 그러나 프랜드로 묶여져 있기 때문에 함수에서 멤버변수를 바로 호출해도 정상적으로 동작한다. 추가적으로 hospital 클래스의 멤버 변수또한 프렌드로 묶여있기 때문에 접근이 가능하다.

그래서 결과가 이렇게 나오게 된다. 

지금은 멤버함수를 프렌드로 선언한 결과이고, 추가적으로 클래스를 프렌드로 선언할 수도 있다. 그래서 다음처럼 코드가 바뀌게 되면

#include <iostream>
using namespace std;

class animal;

class hospital {
	int price;
public:
	hospital(){
		this->price = 1000;
	}
	void get_history(animal a1);
};

class animal {
	string name;
	int age;
public:
	animal(string name) {
		this->name = name;
		this->age = 0;
	}
	friend hospital;
};

void hospital::get_history(animal a1) {
	cout << a1.name << " " << a1.age << "살 진료비는 " << price << "원 입니다.";
}


int main() {
	animal a1("dog");
	hospital doc;
	doc.get_history(a1);
}

클래스 전체를 프렌드로 선언한 상태이고, 정상작동한다.

 

연산자 중복 (function overloading)

간단하게 이야기해서 연산자라고 함은 +, -, /, %, <, > 등이 있다.
당연하게도 이는 숫자끼리의 연산을 담당한다. 근데, 그럼 한가지 예시를 들어보자.

#include <iostream>
using namespace std;

class animal {
	string name;
	int age;
public:
	animal(string name) {
		this->name = name;
		this->age = 0;
	}
};

int main() {
	animal a1("dog");
	animal a2("cat");
	cout << a1 + a2;
}

animal 클래스의 객체 a1과 a2 그리고 이 둘의 + 연산은 무엇을 의미할까? 사실 이는 컴파일이 되지 않는다.

말 그대로 

숫자 연산자 숫자 = 연산자에 따라 계산이 가능
객체 연산자 객체 = ?
클래스 연산자 클래스 = ?
??? 연산자 ??? = ?

이런걸 한 번 구현해보고 싶다에서 시작한게 바로 이 기능이라고 이해하면된다.  그래서 이를 연산자 중복이라고 한다.
더 쉽게 이해하면 앞서 배운 것 중에서 함수의 중복이라는 개념이 있었다. (function overloading) 여기서
1 + 1 = 2 라는 일을 수행하는 + 연산자는 C++ 프로그램을 뜯어보면 이렇게 되어 있을 것이라 예상이된다.

+ 연산 (int a , int b){
	return 정수a 와 정수b를 더한값
}

그러면 우리가 이러한 연산을 새롭게 정의할 수 있을 것이다.

class 동물{
	string name;
public:
	동물(string name){
    	this->name = name;
    }
}

연산자 + (동물 객체1, 동물 객체2){
	return 객체1의 이름 + 객체2의 이름
}

int main(){
	동물1("개")
	동물2("고양이")
    
    cout << 동물1 + 동물2;
}

이 코드의 결론은 "개고양이" 일 것이다. 위의 코드처럼 +연산자를 함수의 중복을 이용하여 다시 정의하는 것을 연산자 중복이라고 한다. 그래서 연산자 중복은 다음의 특징을 가진다.
1. C++언어에서 본래 있는 연산자만 중복 가능하다.
+, -, *, /,==,!=,%,&&  등
2. 연산자 중복은 피연산자의 타입이 다른 연산을 새로 정의하는 것이다.
좌측 혹은 우측에는 무조건 정수나 실수가 아닌 객체가 와야한다.
3. 연산자 중복은 함수를 통해 이루어진다.

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

클래스의 메소드(멤버함수)가 되거나, 프렌드로 하여 전역 함수로 구현하여야한다.
5. 연산자 중복으로 피연산자의 개수를 바꿀 수 없다.
간단하게 1+1   2 가 말이 안되는것처럼 1+1 외에 안된다. 
6. 연산자 중복으로 연산의 우선순위를 바꿀 수 없다.
1+2*2 에서 답은 5가된다. 이 관계를 바꿀 수 없다.
7. 모든 연산자가 중복가능한 것은 아니다.
이는 인터넷에 표가 나와있다.

연산자 함수를 선언해보자

연산자 함수를 구현하기 위해서는 두 가지 약속을 지켜주어야한다.
1. 클래스 내에서 메소드로 구현하기
2. 전역함수로 선언하고 프렌드로 지정하기

 

여튼 여러 이야기는 위에서 한 거 같으니 정말 간단하게 진행을 해보자. 위에서 만들었던 코드를 응용해서 animal의 객체를 더하면 서로의 이름을 더해서 출력해주는 코드를 작성해보자.

#include <iostream>
using namespace std;

class animal {
	string name;
	int age;
public:
	animal() {}
	animal(string name) {
		this->name = name;
		this->age = 0;
	}

	string get_name() {
		return name;
	}

	animal operator + (animal op2) {
		animal temp;
		temp.name = this->name + op2.name;
		temp.age = 0;
		return temp;
	}
};

int main() {
	animal a1("dog");
	animal a2("cat");
	animal result;
	result  =  a1 + a2;
	cout << result.get_name();
}

코드를 잠시 구경해보면 새로운 함수인 animal operator + (animal op2) 라는 함수.. 여튼 연산자를 중복할려면
반환타입 operator  연산자 (매개변수) 
형식을 지켜주면된다. 매개변수를 animal &op2 로 주어도 정상 작동한다.
결과는 코드만 살짝봐도 찍을 수 있듯이 dog와 cat이 합쳐진 객체가 하나생성된다. 

이런 역할을 하는게 연산자 중복의 역할이다. 이를 응용하면 다양한 연산자에 대응할 수 있을 것이다. 우리가 일상적으로 선언하는 모든 연산자에 적용시킬 수 있다. 

추가적으로 위의 연산자 함수는 다음처럼 만들 수도 있다.

animal& operator + (animal op2) {
		name = name + op2.name;
		age = 0;
		return *this;
	}

리턴타입이 animal& 이기 때문에 자신의 참조가 리턴된다.

 

한가지 궁금증이 들 수도 있는 부분은 전위연산자와 후위연산자라고 생각한다.

int main() {
	animal a1("dog");
	animal a2("cat");

	a1++;
}

a1++은 어떻게 구현할까?

#include <iostream>
using namespace std;

class animal {
	string name;
	int age;
public:
	animal() {
		this->age = 0;
	}

	animal(string name) {
		this->name = name;
		this->age = 0;
	}

	string get_name() {
		return name;
	}

	int get_age() {
		return age;
	}

	animal& operator + (animal op2) {
		name = name + op2.name;
		age = 0;
		return *this;
	}

	animal& operator++() {
		age++;
		return *this;
	}

	animal& operator--(int x) {
		animal temp = *this;
		age--;
		return temp;
	}
};

int main() {
	animal a1("dog");
	animal a2("cat");

	cout << a1.get_age() << endl;

	++a1;
	cout << a1.get_age() << endl;
	
	a1--;
	cout << a1.get_age() << endl;
}

다음처럼 구현하면 됩니다. 전위연산의 경우 ++를 확인해보면 됩니다. 후위연산의 경우 --를 확인합니다.
 

연산자 함수와 프렌드를 사용해보자.

아까 배운대로 프렌드를 넣어주고 클래스 밖으로 빼내면 다음처럼 컴파일이 불가능해진다. 생각을해보면 외부 전역함수가 되어버렸기 때문에 객체를 2개 넣어주어야한다.

그래서 다음처럼 수정을 한 함수일때 

friend animal operator + (animal op1, animal op2);
};

animal operator + (animal op1, animal op2) {
	animal temp;
	temp.name = op1.name + op2.name;
	temp.age = 0;
	return temp;
}

int main() {
	animal a1("dog");
	animal a2("cat");
	animal a3;

	a3 = a1 + a2;
	cout << a3.get_name();
}

결과는 a1과 a2의 이름을 더한 값이 나오게된다. 

프렌드를 통해 전위연산이나 후위연산을 한다면 다음처럼 선언한다.

전위
friend animal& operator++(animal& op)
후위
friend animal operator++(animal& op, int = x)

 

참조의 리턴을 연습해보자.

a << 1 << 2 
일때 1과 2를 더하게 만들어보자 a <<1 연산이후 다음 연산이 객체 a에 의해 진행되려면 a << 1 에서 연산자 << 연산 후에 객체 a의 참조를 리턴해야한다. 

#include <iostream>
using namespace std;

class animal {
	string name;
	int age;
public:
	animal() {
		this->age = 0;
	}

	animal(string name) {
		this->name = name;
		this->age = 0;
	}

	int get_age() {
		return age;
	}

	string get_name() {
		return name;
	}

	animal& operator << (int n) {
		age += n;
		return *this;
	}
};

int main() {
	animal a1("dog");	
	a1 << 1 << 2;
	cout << a1.get_age();
}

다음의 코드이다. 만약 참조를 사용하지 않으면, << 후 그에 대한 결과를 복사본에 저장을 하기 때문에 다음 연산은 복사본에 저장을 하게 된다. 다음의 예시를 보자.

	animal operator << (int n) {
		animal temp;
		temp.age = temp.age += n;
		return temp;
	}

결과는 0이 나온다.

그러니 꼭 참조를 사용해야한다.


참고 문헌
Scott Douglas Meyers(2015). Effective C++. Protec Media(프로텍 미디어)
Robert C. Martin(2021).UML 실전에서는 이것만 쓴다(UML for Java Programmers). 인사이트
황기태(2021). 명품 C++ Programming. (주)생능출판사