[C++ Primer Plus] Chapter 15. (1) 클래스 프렌드와 프렌드 멤버 함수, 내포 클래스
Table of Contents
C++ 기초 플러스 책을 읽고 공부한 노트입니다.
클래스 프렌드 #
-
지금까지는 프렌드 함수를 살펴봤는데, 클래스도 프렌드가 될 수 있다.
-
TV 클래스와 리모콘 클래스를 생각해보자.
- 둘은 is-a 관계도 아니며 has-a 관계도 아니다.
- 하지만 리모콘은 TV의 상태를 변경할 수 있다.
- 이것은 리모콘 클래스를 TV 클래스의 프렌드로 만들어야 한다는 것을 암시한다.
-
프렌드 선언은
private
,protected
,public
부분 어디에 두던 상관 없다. -
컴파일러가
Remote
클래스를 처리하기 전에Tv
클래스에 대해 알아야 한다.- 방법
- (1)
Tv
클래스를 먼저 정의한다. (아래 예제) - (2) 사전 선언(forward declaration)
- (1)
- 방법
class Tv // Tv를 먼저 선언해서 Remote가 Tv를 알도록 한다. (1)
{
private:
int state; // on 또는 off
int volume; // 디지털 볼륨이라고 가정한다
int maxchannel; // 최대 채널 수
int channel; // 현재 설정된 채널
int mode; // 지상파 방송 또는 케이블 방송
int input; // TV 입력 또는 DVD 입력
public:
// friend 선언은 어디에 있어도 상관 없다.
friend class Remote; // Tv의 private 부분에 Remote의 모든 메서드들이 접근할 수 있다.
enum { Off, On };
enum { MinVal, MaxVal = 20 };
enum { Antenna, Cable };
enum { TV, DVD };
Tv(int s = Off, int mc = 125)
: state(s), volume(5), maxchannel(mc), channel(2), mode(Cable), input(TV) {}
void OnOff() { state = (state == On) ? Off : On; } // state = state ^ 1; 혹은 state ^= 1; 와 같다. (XOR)
bool IsOn() const { return state == On; }
bool VolUp();
bool VolDown();
void ChanUp();
void ChanDown();
void SetMode() { mode = (mode == Antenna) ? Cable : Antenna; }
void SetInput() { input = (input == TV) ? DVD : TV; }
void Settings() const;
};
class Remote
{
private:
int mode; // TV 컨트롤 또는 or DVD 컨트롤
public:
Remote(int m = Tv::TV) : mode(m) {}
void OnOff(Tv& t) { t.OnOff(); }
bool VolUp(Tv& t) { return t.VolUp(); }
bool VolDown(Tv& t) { return t.VolDown(); }
void ChanUp(Tv& t) { t.ChanUp(); }
void ChanDown(Tv& t) { t.ChanDown(); }
void SetChan(Tv& t, int c) { t.channel = c; } // Tv의 private 멤버인 channel에 접근할 수 있다.
void SetMode(Tv& t) { t.SetMode(); }
void SetInput(Tv& t) { t.SetInput(); }
};
- 만약에 프렌드가 아니었다면?
Tv
클래스의private
부분을public
으로 만들어야 한다.- 혹은
Tv
와Remote
가 함께 들어간 클래스를 만들어야 한다. 이렇게 하면 하나의Remote
로 여러대의Tv
를 제어하는 것을 반영하지 못할 것이다.
프렌드 멤버 함수 #
-
위의 예시에서 유일하게 프렌드 자격이 필요한 부분은
Remote::SetChan()
뿐이다. -
클래스 전체를 프렌드로 하지 않고 필요한 메서드들만 프렌드로 만들면 어떨까?
-
Tv
클래스 안에 프렌드 자격을 부여할Remote::SetChan()
를 선언할 수 있다.- 하지만, 이렇게 하려면 컴파일러가
Remote
클래스 선언을 먼저 알아서, 그 안에SetChan()
메서드가 있다는 걸 알아야 한다. - 그렇다고
Remote
정의를Tv
정의 앞에 두기에는,Remote::SetChan()
이Tv
를 사용하기 때문에Remote
도Tv
를 먼저 알아야 한다.
- 하지만, 이렇게 하려면 컴파일러가
class Tv
{
// Remote 클래스 선언을 먼저 알아야 한다.
friend void Remote::SetChan(Tv& t, int c); // Tv의 private 부분에 Remote의 특정 메서드만 접근할 수 있다.
};
- 이러한 순환 종속을 피하는 방법은, 사전 선언을 사용하는 것이다.
class Tv; // Remote가 Tv를 알도록 한다. (2)
class Remote
{
void Remote::SetChan(Tv& t, int c); // 사전 선언을 했으므로, Remote는 Tv를 알고 있다.
};
class Tv
{
friend void Remote::SetChan(Tv& t, int c); // Tv는 Remote의 선언을 알고 있다.
};
- 순서를 반대로 하는 것은 안 된다.
- 왜냐하면,
Tv
는Remote
안에SetChan()
메서드가 있는 걸 먼저 알아야하기 때문이다.
- 왜냐하면,
// (X)
class Remote;
class Tv
{
friend void Remote::SetChan(Tv& t, int c); // Tv는 Remote 안에 SetChan() 메서드가 있는 걸 알아야한다.
}
class Remote
{
void Remote::SetChan(Tv& t, int c);
}
- 또 다른 문제가 있다.
Remote::OnOff()
에서Tv::OnOff()
를 호출한다.- 이것은
Remote
가Tv
의 클래스 선언을 미리 알아야 하며, 그래서Tv
가 어떤 메서드들을 가지고 있는지 알아야 한다는 것을 의미한다.
class Tv;
class Remote
{
void OnOff(Tv& t)
{
t.OnOff(); // Tv의 클래스 선언을 먼저 알아서 그 안에 OnOff() 메서드가 있다는 걸 알아야 한다.
}
};
class Tv
{
void OnOff();
};
- 해결책
- 인라인 함수로 만들고 싶다면,
-
Remote
클래스 선언 안에는 메서드를 선언만 하고, 정의는Tv
클래스 뒤에 둔다. - 혹은 인라인 함수로 만들지 않고, 정의를
.cpp
파일에 넣는다.
- 인라인 함수로 만들고 싶다면,
-
class Tv;
class Remote
{
void OnOff(Tv& t); // 선언만 한다.
};
class Tv
{
void OnOff();
};
// Tv 뒤에 (Tv 클래스 선언을 알고 나서) 정의를 한다. (인라인)
inline void Remote::OnOff(Tv& t) { t.OnOff(); }
상호 프렌드와 공유 프렌드 #
- 상호 프렌드(mutual friend)
- 각 클래스가 서로에 대해 프렌드이다.
class Tv
{
friend class Remote; // Remote의 모든 메서드는 Tv의 private 부분에 접근 가능하다.
public:
void Buzz(Remote& r); // Remote의 메서드를 사용하려면 Remote 클래스 선언 뒤에 나와야 하므로 일단 선언만한다.
void VolUp() { }
};
class Remote
{
friend class Tv; // Tv의 모든 메서드는 Remote의 private 부분에 접근 가능하다.
public:
void VolUp(Tv & t) { t.VolUp(); }
};
// Remote 뒤에 정의를 둔다.
inline void Tv::Buzz(Remote & r)
{
// ...
}
- 공유 프렌드
- 하나의 함수가 서로 다른 두 클래스의
private
데이터 모두에 접근해야 할 때 사용한다. - 한 클래스의 멤버로 그 함수를 두고, 다른 클래스에서
friend
로 선언할수도 있겠다. - 하지만 때로는, 두 클래스 모두에 대해 프렌드로 만드는 것이 합리적인 경우가 있다.
- 예를 들어, 측정하는 장치인
Probe
클래스와 분석하는 장치인Analyzer
클래스가 있다고 하자. - 각 클래스는 내부에 시계를 하나씩 가지고 있는데, 그 두 시계를 서로 일치시키고 싶다.
- 예를 들어, 측정하는 장치인
- 하나의 함수가 서로 다른 두 클래스의
class Analyzer;
class Probe
{
// Sync 함수들은 Probe의 private 부분에 접근 가능하다.
friend void Sync(Analyzer& a, const Probe& p); // a를 p에 맞춘다.
friend void Sync(Probe& p, const Analyzer& a); // p를 a에 맞춘다.
};
class Analyzer
{
// Sync 함수들은 Analyzer private 부분에 접근 가능하다.
friend void Sync(Analyzer& a, const Probe& p);
friend void Sync(Probe& p, const Analyzer& a);
};
// 프렌드 함수들을 정의한다.
inline void Sync(Analyzer& a, const Probe& p) {}
inline void Sync(Probe& p, const Analyzer& a) {}
내포 클래스 #
- 다른 클래스 안에 선언된 클래스를 내포 클래스라 한다.
- 한 클래스 안에서만 지역적으로 알려진다.
- 일반적으로, 다른 클래스 구현을 지원하거나, 이름 충돌을 막기 위해 사용된다.
class Queue
{
private:
class Node // Queue 클래스 안에 Node 클래스가 내포되었다.
{
public:
int item;
Node * next;
Node(const Item& i) : item(i), next(nullptr) {}
};
public:
void Enqueue(const Item & item)
{
// ...
Node * add = new Node(item); // 좀 더 간단해졌다.
//...
}
};
// 만약에 Node 클래스의 생성자를 .cpp 파일에 넣고 싶다면 이렇게 하면 된다.
// 사용 범위 결정 연산자를 두 번 쓴다.
Queue::Node::Node(const Item& i) : item(i), next(nullptr) {}
- 내포 클래스, 내포 구조체, 내포 열거체의 사용 범위 특성
내포하는 클래스에 선언된 장소 |
내포하는 클래스에서 사용 여부 |
내포하는 클래스에서 파생된 클래스에서 사용 여부 |
바깥 세계에서 사용 여부 |
---|---|---|---|
private 부분 |
O | X | X |
protected 부분 |
O | O | X |
public 부분 |
O | O | O (클래스 제한자 사용) |
class Team
{
public: // public 부분에 선언됨
class Coach {};
};
int main()
{
Team::Coach c; // 바깥에서 클래스 제한자 사용해서 접근 가능
}
- 접근 제어
- 어떤 특정 클래스가 사용 범위 안에 들어오면, 일반적인 접근 제어 규칙(
public
,protected
,private
)이 접근 가능 여부를 결정한다. Queue
예제에서Node
의 모든 데이터가public
으로 선언되었다.- 이것은 일반적인 관행에 위배된다.
- 하지만
Queue
클래스의private
부분에Node
가 선언되었으므로, 바깥 세계에서는 보이지 않는다.
- 어떤 특정 클래스가 사용 범위 안에 들어오면, 일반적인 접근 제어 규칙(
- 템플릿에 내포된 클래스
// QueueTP.h
template <class Item>
class QueueTP
{
private:
enum { Q_SIZE = 10 };
class Node // 내포된 클래스 Node
{
public:
Item item; // 포괄적 데이터형 Item을 사용한다.
Node* next;
Node(const Item& i) :item(i), next(0) { }
};
Node* front;
Node* rear;
int items;
const int qsize;
QueueTP(const QueueTP& q) : qsize(0) {}
QueueTP& operator=(const QueueTP& q) { return *this; }
public:
QueueTP(int qs = Q_SIZE);
~QueueTP();
bool IsEmpty() const { return items == 0; }
bool IsFull() const { return items == qsize; }
int QueueCount() const { return items; }
bool Enqueue(const Item& item);
bool Dequeue(Item& item);
};
template <class Item>
QueueTP<Item>::QueueTP(int qs) : qsize(qs)
{
front = rear = 0;
items = 0;
}
template <class Item>
QueueTP<Item>::~QueueTP()
{
Node* temp;
while (front != 0)
{
temp = front;
front = front->next;
delete temp;
}
}
template <class Item>
bool QueueTP<Item>::Enqueue(const Item& item)
{
if (IsFull()) return false;
Node* add = new Node(item);
items++;
if (front == 0) front = add;
else rear->next = add;
rear = add;
return true;
}
template <class Item>
bool QueueTP<Item>::Dequeue(Item& item)
{
if (front == 0) return false;
item = front->item;
items--;
Node* temp = front;
front = front->next;
delete temp;
if (items == 0) rear = 0;
return true;
}