Skip to main content

[C++ Primer Plus] Chapter 15. (1) 클래스 프렌드와 프렌드 멤버 함수, 내포 클래스

C++ 기초 플러스 책을 읽고 공부한 노트입니다.




클래스 프렌드 #

  • 지금까지는 프렌드 함수를 살펴봤는데, 클래스도 프렌드가 될 수 있다.

  • TV 클래스와 리모콘 클래스를 생각해보자.

    • 둘은 is-a 관계도 아니며 has-a 관계도 아니다.
    • 하지만 리모콘은 TV의 상태를 변경할 수 있다.
    • 이것은 리모콘 클래스를 TV 클래스의 프렌드로 만들어야 한다는 것을 암시한다.

  • 프렌드 선언은 private, protected, public 부분 어디에 두던 상관 없다.

  • 컴파일러가 Remote 클래스를 처리하기 전에 Tv 클래스에 대해 알아야 한다.

    • 방법
      • (1) Tv 클래스를 먼저 정의한다. (아래 예제)
      • (2) 사전 선언(forward declaration)
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으로 만들어야 한다.
    • 혹은 TvRemote가 함께 들어간 클래스를 만들어야 한다. 이렇게 하면 하나의 Remote로 여러대의 Tv를 제어하는 것을 반영하지 못할 것이다.



프렌드 멤버 함수 #

  • 위의 예시에서 유일하게 프렌드 자격이 필요한 부분은 Remote::SetChan() 뿐이다.

  • 클래스 전체를 프렌드로 하지 않고 필요한 메서드들만 프렌드로 만들면 어떨까?

  • Tv 클래스 안에 프렌드 자격을 부여할 Remote::SetChan()를 선언할 수 있다.

    • 하지만, 이렇게 하려면 컴파일러가 Remote 클래스 선언을 먼저 알아서, 그 안에 SetChan() 메서드가 있다는 걸 알아야 한다.
    • 그렇다고 Remote 정의를 Tv 정의 앞에 두기에는, Remote::SetChan()Tv를 사용하기 때문에 RemoteTv를 먼저 알아야 한다.
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의 선언을 알고 있다. 
};

  • 순서를 반대로 하는 것은 안 된다.
    • 왜냐하면, TvRemote 안에 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()를 호출한다.
    • 이것은 RemoteTv의 클래스 선언을 미리 알아야 하며, 그래서 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;
}