프렌드 클래스
하나의 클래스를 다른 클래스의 프렌드로 만들고 싶을 때는 언제일까? 한 예를 살펴보자.
텔레비전과 리모콘을 간단한 시뮬레이션을 프로그래밍해야 한다고 가정하자. 텔레비전을 나타내는 Tv 클래스와 리모콘을 나타내는 Remote 클래스를 정의하기로 결정한다. 분명히 Tv 클래스와 Remote 클래스 사이에는 어떤 종류의 관계가 있어야 한다. 대체 그것은 어떤 종류일까? 리모콘은 텔레비전이 아니고, 텔레비전도 리모콘이 아니다. 따라서 public 상속의 is-a 관계가 적용되지 않는다. 또한 리모콘 텔레비전의 한 성분이 아니고, 텔레비전도 리모콘의 한 성분이 아니다. 그러므로 컨텐이먼트 상속 또는 private 상속이나 protected 상속의 has-a 관계도 적용되지 않는다. 그런데 리모콘은 텔레비전의 상태를 변경할 수 있다. 이것이 Remote 클래스를 Tv 클래스의 프렌드로 만들어야 한다는 것을 암시한다.
friend class Remote;
프렌드 선언은 public, private, protected 부분 어디에나 둘 수 있다. 위치에 다른 차이는 없다. Remote 클래스는 Tv클래스를 들먹이기 때문에, 컴파일러는 Remote클래스를 처리하기 전에 Tv 클래스에 대해 알아야 한다. 이 일을 가장 쉽게 하는 방법은, Tv 클래스를 먼저 정의하는 것이다. 다른 방법으로, 사전 선언 (forward decaration)을 사용할 수도 있다.
프렌드 멤버 함수
클래스 전체를 프렌드로 만드는 대신, 원하는 메서드들만 다른 클래스에 대해 프렌드로 만드는 것이 가능하다. 그러나 그것은 조금 다루기 어렵다. 선언들과 정의들을 배치하는 순서에 신경을 써야 한다. 그 이유를 알아보자.
Remote::set_chan()을 Tv 클래스에 대해 프렌드로 만드는 방법은, Tv클래스 선언 안에 그것을 프렌드로 선언하는 것이다.
class Tv
{
friend void Remote::set_chan(Tv & t, int c);
...
};
그러나 컴파일러가 이 구문을 처리하려면, 컴파일러가 Remote 정의를 미리 알고 있어야 한다. 만약 그렇지 않다면, 컴파일러는 Remote가 클래스이고, set_chan()이 Remote 클래스의 메서드라는 사실을 알 수 없게 된다. 따라서 Remote 정의가 Tv 정의 앞에 와야한다. 하지만 Remote 메서드들이 Tv 객체를 들먹인다는 사실은, Tv 정의가 Remote 정의 앞에 나타나야 한다는 것을 의미한다. 이러한 순환 종속을 피하는 방법은, 사전 선언(forward declaration)을 사용하는 것이다. 그렇게 하려면, 다음과 같은 구문을 Remote 정의 위에 삽입해야 한다.
class TGv; // 사전 선언
결과적으로 선언과 정의들은 다음과 같은 순서로 배치된다.
class Tv; // 사전 선언
class Remote {...};
class Tv {...};
대답은 '아니오'이다. 그 이유는 앞에서도 설명했듯이, Tv 클래스 선언 안에 어떤 Remote 메서드가 프렌드로 선언되어 있다는 사실을 컴파일러가 알게 될 때, 컴파일러는 이미 Remote 클래스 선언, 특히 set_chan() 메서드 선언을 알고 있어야 하기 때문이다.
또 한 가지 어려운 문제가 남아 있다. Remote 선언은 다음과 같은 인라인 코드를 포함하고 있다.
void onoff(Tv & t){t.onoff();}
이것은 Tv 메서드를 호출하기 때문에, 이 시점에서 컴파일러는 Tv 클래스 선언을 이미 알고 있고, 그래서 Tv가 어떤 메서드들을 가지고 있는지도 알고 있어야 한다. 그러나 방금 전에도 살펴보았듯이, Tv 선언은 반드시 Remote 선언 뒤에 와야한다. 이 문제의 해결책은, Remote를 메서드 선언(declaration)으로 제한하고, 실제 정의(definition)들은 Tv 클래스 뒤에 두는 것이다. 결과적으로 다음과 같은 순서로 배치 된다.
class Tv; // 사전 선언
class Remote{...}; // Tv를 사용하는 Remote 메서드를 원형
class Tv{...}; // 여기에 Remote 메서드들의 실제 정의들을 넣는다.
Remote 메서들의 원형은 다음과 같을 것이다.
void onoff(Tv & t);
이 원형을 조사할 때, 컴파일러는 사전 선언을 통해 Tv가 클래스라는 사실을 알고 있다. Remote 메서드들의 실제 정의에 도달하는 시점에서, 컴파일러는 Tv 클래스 선언을 이미 알고 있고, Remote 메서드들을 컴파일하는 데 필요한 추가 정보들도 알고 있다. 메서드 정의에 inline 키워드를 사용함으로써, 그 메서드들을 여전히 인라인 메서드로 만들 수 있다.
그 밖의 프렌드 관계
Remote의 어떤 메서드들이 Tv 객체에 영항을 줄 수 있고, Tv의 어떤 매서드들이 Remote 객체에 영항을 줄 수 있는, 상호 프렌드(mutual friend) 관게를 이용한다. 이를 위해 그 클래스들을 서로 간에 프렌드로 만들 수 있다. 즉, Tv가 Remote에 대한 프렌드이고, Remote는 Tv에 대한 프렌드 이다. 한가지 명심할 것은, Remote 객체를 사용하는 Tv 메서드는 Remote 클래스 선언 앞에 원형을 둘 수 있지만, 컴파일러가 그 메서드를 컴파일하는 데 충분한 정보를 가질 수 있도록, Remote 클래스 선언 뒤에 정의를 두어야 한다는 것이다. 결과적으로 그 설정은 다음과 같이 될 것이다.
class Tv
{
friend class Remote;
public:
void buzz(Remote & r);
...
};
class Remote
{
friend class Tv;
public
void bool volup(Tv & t){t.volup();}
...
};
inline void Tv:buzz(Remote & r)
{
...
}
공유 프렌드
프렌드의 또다른 용도는, 하나의 함수가 서로 다른 두 클래스에 들어 있는 private데이터에 접근해야 할 때이다. 때로는 두 클래스 모두에 대한 프렌드로 만드는 것이 함리적인 경우가 있다. 예를 들면, 프로그램이 가능한 어떤 측정 장치를 나타내는 Probe 클래스와, 프로그램이 가능한 어떤 분석 장치를 나타내는 Analyzer 클래스를 가지고 있다고 가정하자, 각 클래스는 내부 시계를 하나씩 가지고 있다. 두 시계를 서로 일치시킬 수 있게 하고 싶다. 그렇다면 다음과 같은 코드를 사용할 수 있다.
class Analyzer; // 사전 선언
class Probe
{
friend void sync(Analyzer & a, const Probe & p); // a를 p에 마자춘다.
friend void sync(Probe & p, const Analyzer & a); // p를 a에 맞춘다.
...
};
class Analyzer
{
friend void sync(Analyaer & a, const Probe & p); // a를 p에 맞춘다.
friend void sync(Probe & a, const Analyaer & p); // p를 a에 맞춘다.
...
};
내포 클래스
C++에서는 클래스 선언을 다른 클래스 안에 내포시킬 수 있다. 다른 클래스 안에 선언된 클래스를 내포 클래스(nested class)라 한다. 이것은 새로운 데이터형에 클래스 사용 범위를 제공함으로써 이름이 난잡해지는 것을 막는다., 클래스 선언을 내포하고 있는 클래스의 멤버 함수들은, 내포 클래스의 객체들을 생성하여 사용할 수 있다.
내포 클래스는 컨테인먼트와 다르다. 컨테인먼트는 어떤 클래스 객체를 다른 클래스의 멤버로 가지는 것이다. 그와 반면에, 클래스를 내포시키는 것을 클래스 멤버를 생성하지 않는다. 그 대신에 내포 클래스 선은을 내포하고 있는 클래스에만 지역적으로 알려지는 하나의 데이터형을 정의한다.
클래스를 내포시키는 일반적인 이유는, 다른 클래스의 구현을 지원하고, 이름 충돌을 막는 것이다. Queue클래스 예제는 구조체 정의를 내포시킴으로써 얼굴을 가린 내포 클래스의 에를 제공한다.
class Queue
{
// 클래스 사용 범위의 정의들
// Node는 이 클래스에 지역적인, 내포된 구조체 정의이다.
struct Node { Item item; struct Node * newxt;};
...
};
구조체는 멤버들이 디폴트로 public인 클래스이기 때문에, 여기서 Node는 사실상 내포 클래스 선언이다. 그러나 이 정의 클래스 기능을 활요하지 않는다. 특히 거기에는 명시적 생성자가 없다. 이제 그것을 고쳐보자.
먼저, Queue 예제에서 Node 객체들이 어디서 생성되는지 알아볼 필요가 있다. 살펴보면 Node 객체가 생성되는 장소는 유일하게 enqueue() 메서드 내부라는 사실을 알 수 있다.
bool Queue::enqueue(const Item & item)
{
if (isfull())
return false;
Node * add = new Node; // 노드를 생성한다.
// 실패하면, new 연산자는 std::bad_alloc 예외를 발생시킨다.
add->item = item;
add->next = Null;
...
}
이 코드는 Node를 생성한 후에 Node 멤버들에 명시적으로 값을 대입한다. 이것은 생성자에 의해 더 적절하게 이루어질 수 있는 작업이다.
생성자를 언제 어디서 사용해야 하는지 알게 되었으므로, 이제 적절한 생성자 정의를 제공할 수 있다.
class Queue
{
// 클래스 사용 범위의 정의들
// Node는 이 클래스에 지역적인, 내포클래스 정이이다.
class Node
{
public:
Item item;
Node * next;
Mode(const Item & i) : item(o), next(0) {}
};
...
};
이 생성자는 노드의 item 멤버를 i로 초기화하고, next 포인터를 0으로 설정한다. 포인터를 0으로 설정하는 것은 C++에서 널 포인터를 사용하는 한 가지 방법이다. Queue 클래스에 의해 생성되는 모든 노드들은, 처음에 널 포인터로 설정되는 next를 가진다. 이것이 그 클래스가 요구하는 유일한 생성자이다.
그 다음에, 이 생성자를 사용하여 enqueue()를 다시 작성할 필요가 있다.
bool Queue::enqueue(const Item & item)
{
if (isfull()
return false;
Node * add = new Node(item); // 노드를 생성하고, 초기화 한다.
}
이것은, 프로그래머들이 무엇을 해야하는지 정확하게 기억할 것을 요구하지 않고 초기화를 자동화하기 때문에, enqueue()코드를 더 간단하고 안전하게 만단다.
이 예제는 생성자를 클래스 선언 안에 정의한다. 그렇게 하지 않고 메서드 정의 파일에 생성자를 정의하고 싶다면 어떻게 해야 할까? 이 경우에, 그 정의는 Node클래스가 Queue클래스 내부에 정의된다는 사실을 반영해야 한다. 따라서 다음과 같이 사용 범위 결정 연산자를 두 번 사용하면 된다.
Queue::Node::Node(const Item & i) : item(i), next(0) { }
내포 클래스와 접근
내포 클래스에는 두 종류의 접근 형태가 있다.
첫째, 내포 클래스가 선언된 장소가 내포 클래스의 사용 범위를 제한한다. 즉, 내포 클래스가 선언된 장소가 프로그램의 어느 부분에서 내포 클래스의 객테를 생성할 수 있는지를 결정한다.
둘째, 모든 클래스가 다 그렇듯이, 내포 클래스의 public, protected, private 부분들도 그 클래스 멤버에 대한 접근을 제한한다. 내포 클래스를 어디에서 어떻게 사용하느냐 하는 것은, 사용 범위와 접근 제어 둘 다에 달려 있다.
제2 클래스에서 선언된 장소 | 제2 클래스에서 사용 여부 | 제2 클래스로부터 파생된 클래스에서 사용 여부 | 바깥 세계에서 사용 여부 |
private 부분 | O | X | X |
protected 부분 | O | O | X |
public 부분 | O | O | O 클래스 제한자 사용 |
'C++ > C++ 기초 플러스 6판(Book)' 카테고리의 다른 글
[C++] 포인터 선언과 초기화 (0) | 2025.03.22 |
---|---|
[C++] C++ 프로그래밍 철학: 포인터와 템플릿이 존재하는 이유(객체 지향과 포인터의 관계) (0) | 2025.03.21 |
[C++] 코드의 재활용(클래스 템플릿) (0) | 2025.03.12 |
[C++] 코드의 재활용(다중 상속) (0) | 2025.03.11 |
[C++] 클래스의 상속 (0) | 2025.03.10 |