private
) 멤버는 오직 자신의 클래스 멤버에서만 접근할 수 있음을 보았다. 좋은 일이다. 캡슐화와 데이터 은닉을 강제하기 때문이다. 기능을 클래스 안에 캡슐화해 넣어서 여러 책임이 노출되는 것을 방지한다. 데이터를 감춤으로써 클래스의 데이터 정합성을 증진하고 소프트웨어의 다른 부분이 클래스에 속한 데이터에 의존해 구현되지 못하도록 방지한다.
이번의 (아주) 짧은 장을 통하여 friend
키워드와 그 사용법에 깔린 원리를 소개한다. friend
키워드가 있으면 함수는 클래스의 비밀(private
) 멤버에 접근할 수 있다. 그렇다고 해서 데이터 은닉이라는 원칙을 포기한다는 뜻은 아니다.
이 장은 클래스 사이의 친구 관계는 다루지 않는다. 클래스 사이에 친구 관계가 자연스러운 상황은 제 17장과 제 21장에 다루며 그런 상황은 함수 사이의 친구 관계를 다루는 방법을 자연스럽게 확장하면 된다.
친구 사이를 선언하려면 (즉, friend
키워드를 사용하려면) 정의가 잘된 개념적 이유가 충분해야 한다. 전통적으로 클래스 개념의 정의는 보통 다음과 같다.
클래스는 데이터 집합과 거기에 작용하는 함수들을 모은 데이터 구조이다.
제 11장에서 보았듯이 연산자 함수는 클래스 인터페이스 밖에 정의할 필요가 있다. 피연산자들에 대하여 승격을 사용할 수 있고 또는 직접 통제하에 있지 않은 기존 클래스의 편의기능들을 확장할 수 있기 때문이다. 위의 전통적인 클래스 개념의 정의에 따르면 클래스 인터페이스 안에 정의하지 않더라도 그 함수들은 그 클래스에 속해 있다고 간주해야 한다. 다시 말해, 언어 구문이 허용했다면 틀림없이 그 함수들은 클래스 인터페이스 안에 정의되었을 것이다.
그런 함수를 두 가지 방식으로 구현할 수 있다. 하나는 공개 멤버 함수를 사용하여 구현하는 것이다. 이 접근법은 11.2절에 사용했다.
또다른 접근법은 클래스 개념의 정의를 적용하는 것이다. 그런 함수들이 실제로 그 클래스에 속해 있다고 서술하면 그들에게는 객체의 데이터 멤버에 직접 접근을 허용해야 한다. 이런 목적은 friend
키워드를 사용하여 달성한다.
일반 원칙으로서 같은 파일에 클래스 인터페이스로 선언되어 있으면 클래스 객체의 데이터에 작용하는 모든 함수들은 그 클래스에 속해 있으며 그 클래스의 데이터 멤버에 직접 접근할 수 있다.
Person
클래스의 삽입 연산자는 다음과 같이 구현되었다 (9.3절).
ostream &operator<<(ostream &out, Person const &person) { return out << "Name: " << person.name() << ", " "Address: " << person.address() << ", " "Phone: " << person.phone(); }이제
Person
객체를 스트림 안으로 삽입할 수 있다.
그렇지만 이 구현은 세 개의 멤버 함수를 호출하기를 요구한다. 이것은 비효율의 근원으로 볼 수 있다. 개선하는 방법은 Person::insertInto
멤버를 구현하고 operator<<
가 그 함수를 호출하도록 만드는 것이다. 이 두 함수는 다음과 같이 정의할 수 있다.
std::ostream &operator<<(std::ostream &out, Person const &person) { return person.insertInto(out); } std::ostream &Person::insertInto(std::ostream &out) { return out << "Name: " << d_name << ", " "Address: " << d_address << ", " "Phone: " << d_phone; }
insertInto
는 멤버 함수이므로 객체의 데이터 멤버에 직접 접근한다. 그래서 person
을 out
안으로 삽입할 때 멤버 함수를 따로 더 호출하지 않아도 된다.
다음 단계는 insertInto
가 오직 operator<<
를 위해서만 정의되어 있고 그리고 operator<<
는 Person
의 클래스 인터페이스를 포함하고 있는 헤더 파일에 선언되어 있으므로 Person
클래스에 속한 함수로 간주해야 한다는 사실을 깨닫는 것이다. 그러므로 insertInto
는 operator<<
를 친구로 선언하면 없어도 된다.
친구 함수는 클래스 인터페이스에 친구로 선언해야 한다. 이 친구 선언은 멤버 함수가 아니므로 클래스의 private
과 protected
그리고 public
구역에 영향을 받지 않는다. 친구 선언은 클래스 인터페이스 어디에든 배치할 수 있다. 그러나 관례적으로 친구 선언은 클래스 인터페이스 윗쪽에 놓인다. Person
클래스는 다음과 같이 추출 연산자와 삽입 연산자에 friend
를 선언하면서 시작한다.
class Person { friend std::ostream &operator<<(std::ostream &out, Person &pd); friend std::istream &operator>>(std::istream &in, Person &pd); // 이전에 보인 인터페이스 (데이터와 함수) };이제 삽입 연산자는 직접
Person
객체의 데이터 멤버에 접근이 가능하다.
std::ostream &operator<<(std::ostream &out, Person const &person) { return cout << "Name: " << person.d_name << ", " "Address: " << person.d_address << ", " "Phone: " << person.d_phone; }친구 선언은 진짜 선언이다. 클래스에 친구 선언이 있으면 이 친구 함수는 다시 클래스 인터페이스 아래에 선언할 필요가 없다. 이것은 또한 클래스 설계자의 의도를 확실하게 알려준다. 친구 함수가 클래스에 선언되어 있으므로 그 클래스에 속해 있다고 간주할 수 있다.
class
키워드가 없어도 클래스를 친구로 선언할 수 있다. 예를 들어,
class Friend; // 클래스 선언 typedef Friend FriendType; // 그에 대한 typedef using FName = Friend; // using 선언 class Class1 { friend Friend; // FriendType 그리고 FName: 역시 OK };C++11 표준 이전에서 친구 선언은
friend class Friend
와 같이 명시적으로 class
를 요구한다.
컴파일러가 아직 친구의 이름을 보지 못했다면 class
를 명시적으로 사용해야 한다. 예를 들어
class Class1 { // friend Unseen; // 컴파일 실패: Unseen이 무슨 유형인지 모름. friend class Unseen; // OK };22.10절에 확장 친구 선언을 클래스 템플릿에 사용하는 법을 다룬다.