[C++] friend | 프렌드
\(\texttt{friend}\)
프렌드
- 멤버 함수가 아닌 외부 함수를 해당 클래스의 전체 멤버에 접근 가능하게 만들어서* 해당 클래스의 멤버 함수와 동등한 접근 권한을 갖게 하는 C++의 키워드이다.
ex) 클래스 외부에 정의된 일반 함수를 \(\texttt{friend}\)를 통해 클래스 내부로 Grant하면, 클래스의 멤버 함수인 것처럼 클래스의 멤버에 접근이 가능해진다.
ex) A클래스에 B클래스를 \(\texttt{friend}\)로 Grant하면, B클래스의 멤버 함수들은 A클래스의 멤버 함수인 것 처럼 동작할 수 있다.
- \(\texttt{friend}\) 키워드로 Grant 가능한 형태로 프렌드 함수, 프렌드 클래스 ,프렌드 멤버 함수 세 가지가 있다.
※ 프렌드 함수 개념을 이용하여, 어떤 클래스의 멤버 함수를 \(\texttt{friend}\)를 통해 Grant할 수도 있다.
* 이 때, 접근 가능하게 하는 동작을 "\(\texttt{friend}\)로 Grant 하다."라고 표현한다.
\(\texttt{friend}\) 함수 정의
- Grant하고자 하는 클래스 선언부에 Grant할 \(\texttt{friend}\) 함수의 원형을 넣어야 한다.
- \(\texttt{friend}\) 키워드는 클래스 내에 들어가는 함수 원형에 붙으며, 함수 정의 부분에는 붙지 않는다.
- \(\texttt{friend}\) 함수는 기본적으로 멤버 함수가 아니기 때문에, \(\texttt{::}\)연산자를 사용할 수 없다.
- \(\texttt{friend}\) 함수는 기본적으로 멤버 함수가 아니기 때문에, 호출시에 멤버 연산자(\(\texttt{.}\) 연산자)를 사용할 수 없다.
// Ex. friend 함수의 원형, 정의, 호출 예시
class Time {
private:
...
public:
...
friend Time operator* (double, const Time&); // friend 키워드는 함수 원형에서만 사용한다.
// 또한, operator*() 함수가 외부 함수임을 알 수 있는 대목이기도 하다.
};
...
// 태생이 일반함수 이므로, 정의부에는 friend 키워드가 붙지 않는다.
Time operator* (double mult, const Time& t) { // 일반 함수 이므로 Time:: 제한자 또한 붙지 않는다.
Time result;
long totalMinutes = t.hours * mult * 60 + t.minutes * mult;
result.hours = totalMinutes / 60;
result.minutes = totalMinutes % 60;
return result;
}
...
Time A, B;
// A와 B는 적절한 값으로 초기화되었다 가정하자.
A = 2.75 *B;
// 위 구문은 컴파일러가 A = operator*(2.75, B); 로 재해석한다.
2.75.operator*(b); // 이 구문은 당연히 안된다.
※ 교환법칙이 성립하는 연산자를 오버로딩하는 경우, 구현을 간단히 하는 방법
// Ex. 교환법칙이 성립하는 연산자를 쉽게 오버로딩 하는 예시
class Time {
...
friend Time operator+(const Time&, double); // 세부구현을 담당하는 함수만 Grant되면 된다.
friend Time operator+(double, const Time&); // 꼭 Grant되어야 하는 건 아니지만, Grant할 것을 권장한다.
};
Time operator+(const Time& t, double mult) const {...} // 세부구현을 담당하는 외부 연산자 함수
Time operator+(double mult, const Time& t) const { // 이 버전은 굳이 friend로 Grant될 필요는 없다.
return t * munlt; // (객체 * double상수)는 이미 정의되어 있으므로, 간소하게 표현할 수 있다.
}
- 두 번째 버전의 연산자 함수를 굳이 Grant하는 이유는, 해당 연산자 함수도 클래스의 인터페이스임을 명시하고, 추후에 두 번째 버전의 연산자 함수를 수정할 때, \(\texttt{private}\) 멤버에 접근해야하는 경우가 생길 수도 있기 때문이다.
Friend Classes (프렌드 클래스)
- 클래스 또한, \(\texttt{friend}\)를 통해 어떤 클래스에 Grant될 수 있다.
- Grant된 클래스는 해당 클래스의 public 멤버는 물론 private, protected 멤버에도 접근할 수 있다.
- \(\texttt{friend}\) 선언은 public, protected, private 부분 어디에나 위치시킬 수 있으며, 차이는 없다.
(즉, Grant 선언의 접근 지정자 별 차이는 없다.)
※ 프렌드 함수 개념을 이용하여, 어떤 클래스의 멤버 함수를 \(\texttt{friend}\)를 통해 Grant할 수도 있다.
Ex. Tv 클래스에 Grant된 Remote 클래스
class Tv {
public:
friend class Remote;
Tv() : volume(5) {}
bool volup();
bool voldown();
private:
int volume;
};
class Remote {
public:
Remote() {}
bool volup(Tv& t) { return t.volup(); }
bool voldown(Tv& t) {return t.voldown(); }
};
bool Tv::volup() {
if (volume < 20) {
volume++;
return true;
}
else
return false;
}
bool Tv::voldown() {
if (volume > 0) {
volume--;
return true;
}
else
return false;
}
- 클래스 프렌드는 위와 같이, 클래스 간의 특정한 관계를 표현하는데 유용하며,
이 같은 수단이 없다면, Tv 클래스의 private 부분들을 public으로 구현해야 하거나,
Tv와 Remote가 함께 들어있는 크기가 큰 클래스를 구현해야 할 것이다.
(또한, 두 클래스를 합쳐서 구현하면, 하나의 리모콘으로 여러 텔레비전을 제어하게 할 수 없게 된다.)
- 프렌드 멤버 함수와 달리, 프렌드 클래스는 Forward Declaration을 필요로하지 않는다.
(Tv클래스 내부의 friend 구문이 Remote라는 클래스가 존재함을 컴파일러에게 알리는 역할도 하기 때문이다.)
Friend Member Functions (프렌드 멤버 함수)
- 클래스 전체를 프렌드로 Grant하는 대신, 클래스의 몇몇 메서드만 다른 클래스에 Grant하는 것이 가능하다.
class Tv {
friend void Remote::func();
...
};
- 메서드를 Grant하는 경우, 적절한 Forward Declaration이 필요하다.
class Tv;
class Remote {
void func(); // Grant할 함수의 원형이라도 반드시 선언되어 있어야 한다.
...
};
class Tv {
friend void Remote::func();
...
};
void Remote::func() {
...
}
※ private 멤버를 다루는 메서드는 해당 클래스의 멤버 함수로 구현하고,
public 멤버를 다루는 메서드는 프렌드 클래스에서 구현하여 Grant하는 것이 범용적인 방법이다.
Mutual Friend (상호 프렌드)
- 두 클래스가 서로를 Grant한 관계를 의미한다.
- 이 경우에는 가급적 메서드 정의를 클래스 선언 뒤에 위치시키는 것이 바람직하다.
class Tv {
friend class Remote;
public:
void func(Remote& r); // func의 정의는 Remote 클래스 선언 뒤에 행해져야 한다.
...
};
class Remote {
friend class Tv;
...
};
void Tv::func(Remote& r) {
...
}
Shared Friend (공유 프렌드)
- 하나의 함수를 두 클래스에 동시에 Grant시키는 관계를 의미한다.
- 이를 통해 해당 함수는 두 클래스의 private 멤버에 모두 접근할 수 있게 된다.
class Analyzer;
class Probe {
friend void sync(Analyzer& a, const Probe& p);
friend void sync(Probe& p, const Analyzer& a);
...
};
class Analyzer {
friend void sync(Analyzer& a, const Probe& p);
friend void sync(Probe& p, const Analyzer& a);
...
};
Nested Class (내포 클래스)
- 클래스 내부에 선언된 클래스를 의미한다.
- 클래스를 Class Scope를 갖게 함으로써 이름 중복을 방지한다.
- 내포 클래스가 public 부분에 선언되고, 사용 범위 결정 연산자(::)를 이용하면 외부에서도 내포 클래스 객체를 사용할 수 있다.
class Queue {
class Node {
public:
int data;
Node* next;
Node(const int& i) : data(i), next(0) {}
};
...
};
- 외부 파일에서 내포 클래스의 메서드를 정의하기 위해선 사용 범위 결정 연산자(::)를 통해 범위를 명확히 밝혀야 한다.
Queue::Node::Node(const int& i) : data(i), next(0) {
...
}
내포 클래스의 Scope
private 부분에 선언된 내포 클래스: 포함한 클래스에서만 사용할 수 있다.
protected 부분에 선언된 내포 클래스: 포함한 클래스, 포함한 클래스에서 파생된 클래스에서만 사용할 수 있다.
public 부분에 선언된 내포 클래스: 클래스 외부에서도 제한자(::)를 통해 사용할 수 있다.
※ 이와 같은 Scope는 내포 구조체, 내포 열거체에도 동일하게 적용된다.
class Team {
public:
class Coach {...};
...
};
// Team 클래스 외부에서의 내포 클래스 객체 선언
Team::Coach forhire;
내포 클래스와 포함한 클래스 간의 Access Control
- 클래스를 내포시키는 것과 접근 제어는 근본적으로 관계가 없다.
- 즉, Node 클래스를 포함한 Queue 클래스라고 해서 Node 클래스에 자유롭게 접근할 수 있지 않고,
반대의 경우도 마찬가지이다.
- 클래스 내포는 내포한 클래스의 Scope만을 통제하는 기능이다.
- 어차피, 내포된 클래스는 포함한 클래스의 Scope를 가지므로,
내포할 클래스의 모든 멤버를 public으로 선언하는 것이 편리하다.
(위의 Queue 클래스에 내포된 Node의 모든 멤버가 public 부분에 선언된 이유이다.)