string
클래스에 string::iterator
유형을 사용할 수 있다. string
안에 저장된 모든 문자를 제공한다. string::iterator
유형은 iterator
객체로 정의하고 string
클래스 안에 내포 클래스로 정의할 수 있다.
내포 클래스는 둘레의 클래스 안 어디에든 배치할 수 있다. 공개 구역이나 보호 구역 또는 비밀 구역에 상관이 없다. 내포 클래스는 둘레 클래스의 멤버로 간주해도 된다. 클래스에 적용되는 규칙과 접근 방식이 내포 클래스에도 적용된다. 클래스가 다른 클래스의 공개 구역에 포함되면 둘레 클래스 밖에서 볼 수 있다. 보호 구역에 포함되면 그 아래의 파생 클래스에서 볼 수 있다. 비밀 구역에 내포되면 둘레 클래스의 멤버만 볼 수 있다.
둘레 클래스는 내포 클래스에 대하여 특별한 권한이 없다. 내포 클래스는 둘레 클래스가 자신의 멤버에 접근하는 것을 완전히 통제한다. 예를 들어 다음 클래스 정의를 연구해 보자:
class Surround { public: class FirstWithin { int d_variable; public: FirstWithin(); int var() const; }; private: class SecondWithin { int d_variable; public: SecondWithin(); int var() const; }; }; inline int Surround::FirstWithin::var() const { return d_variable; } inline int Surround::SecondWithin::var() const { return d_variable; }내포 클래스 멤버의 접근 규칙은 다음과 같이 정의된다.
FirstWithin
클래스는 Surround
클래스의 안과 밖에서 볼 수 있다. 그래서 FirstWithin
클래스는 전역적으로 가시성이 있다.
FirstWithin
의 생성자와 그의 var
멤버 함수도 전역적으로 볼 수 있다.
d_variable
데이터 멤버는 FirstWithin
클래스의 멤버만 볼 수 있다. Surround
의 멤버도 SecondWithin
의 멤버도 FirstWithin::d_variable
변수에 직접 접근할 수 없다.
SecondWithin
클래스는 Surround
클래스 안에서만 볼 수 있다. SecondWithin
클래스의 공개 멤버는 FirstWithin
의 멤버가 사용할 수 있다. 내포 클래스는 둘레 클래스의 멤버로 간주할 수 있기 때문이다.
SecondWithin
의 생성자와 var
멤버 함수는 Surround
의 (그리고 그의 내포 클래스의) 멤버만 볼 수 있다.
SecondWithin::d_variable
는 SecondWithin
의 멤버에게만 보인다. Surround
멤버도 FirstWithin
의 멤버도 SecondWithin
클래스의 d_variable
에 직접 접근할 수 있다.
friend
)로 선언하면 된다 (17.3절).
내포 클래스는 둘레 클래스의 멤버로 간주할 수 있지만 내포 클래스의 멤버는 둘레 클래스의 멤버가 아니다. 그래서 Surround
클래스의 멤버는 FirstWithin::var
에 직접 접근할 수 없다. 이것은 Surround
객체가 FirstWithin
객체도 아니고 SecondWithin
객체도 아닌 것을 고려하면 이해할 수 있다. 사실, 내포 클래스는 그저 유형의 이름(typenames)일 뿐이다. 내포 클래스의 객체가 자동으로 둘레 클래스 안에 존재하는 것은 아니라는 뜻이 아니다. 둘레 클래스의 멤버가 내포 클래스의 (비-정적) 멤버를 사용해야 한다면 둘레 클래스는 내포 클래스 실체를 정의해야 한다. 둘레 클래스의 멤버는 이를 이용하여 그의 멤버를 사용할 수 있다.
예를 들어 다음 클래스 정의에 둘레 클래스로 Outer
가 있고 내포 클래스로 Inner
가 있다. Outer
클래스는 caller
멤버 함수가 있다. caller
멤버 함수는 Outer
객체 안에 조립된 d_inner
객체를 사용하여 Inner::infunction
멤버를 호출한다.
class Outer { public: void caller(); private: class Inner { public: void infunction(); }; Inner d_inner; // Inner 클래스를 이미 알고 있어야 한다. }; void Outer::caller() { d_inner.infunction(); }
Inner::infunction
멤버는 Outer::caller
멤버의 인라인 정의의 일부로 호출할 수 있다. Inner
클래스의 정의가 아직 컴파일러에게 보이지 않지만 말이다. 반면에 컴파일러는 Inner
클래스의 데이터 멤버가 정의되기 전에 먼저 그 클래스의 정의를 미리 보았어야 한다.
Outer::caller
멤버 함수를 Outer
클래스의 밖에 정의하려면 그 함수의 이름을 (둘레 클래스 영역으로부터 시작하여 (Outer
)) 완전히 자격을 갖추어 컴파일러에게 제공해야 한다. 인라인 함수와 인-클래스 함수도 그에 맞게 정의할 수 있다. 그러면 내포 클래스를 얼마든지 사용할 수 있다. 내포 클래스의 정의가 나중에 둘레 클래스 인터페이스에 나타날지라도 말이다.
(내포된) 멤버 함수가 인라인으로 선언되어 있으면 그 정의는 클래스 인터페이스보다 아래에 배치해야 한다. 내포된 정적 데이터 멤버도 클래스 밖에 정의하는 것이 보통이다. FirstWithin
클래스에 static size_t
epoch
데이터 멤버가 있다면 다음과 같이 초기화할 수 있다.
size_t Surround::FirstWithin::epoch = 1970;둘레 클래스의 바깥 코드에 있는 공개 정적 멤버를 참조하려면 영역 결정 연산자가 여러 개 필요하다.
void showEpoch() { cout << Surround::FirstWithin::epoch; }
Surround
클래스 안에서 FirstWithin::
영역만 사용해야 한다. FirstWithin
클래스 안에서는 명시적으로 영역을 참조할 필요가 없다.
SecondWithin
클래스의 멤버는 어떤가? FirstWithin
클래스와 SecondWithin
클래스는 둘 다 Surround
안에 내포되어 있고 둘레 클래스의 멤버로 간주할 수 있다. 클래스의 멤버는 서로 직접 참조가 가능하므로 SecondWithin
클래스의 멤버는 FirstWithin
클래스의 공개 멤버를 참조할 수 있다. 결과적으로 SecondWithin
클래스의 멤버는 FirstWithin
의 epoch
멤버에 FirstWithin::epoch
와 같이 접근할 수 있다.
예를 들어 다음 Outer
클래스 안에 Inner1
과 Inner2
두 개의 내포 클래스가 있다. Inner1
클래스 안에 Inner2
객체를 가리키는 포인터가 있고 Inner2
클래스에 Inner1
의 객체를 가리키는 포인터가 있다. 교차 참조는 전방 선언이 요구된다. 전방 선언은 정의에 지정한 접근 권한과 동일하게 접근 권한을 주어야 한다. 다음 예제에서 Inner2
전방 선언은 비밀 구역에 주어졌다. 마찬가지로 그의 정의도 Outer
클래스의 비밀 인터페이스에 포함된다.
class Outer { private: class Inner2; // 전방 선언 class Inner1 { Inner2 *pi2; // Inner2의 실체를 가리킨다. }; class Inner2 { Inner1 *pi1; // Inner1의 실체를 가리킨다. }; };
friend
키워드를 사용해야 한다.
친구 선언을 하지 않아도 내포 클래스는 둘레 클래스의 비밀 멤버에 접근할 권한이 있음을 눈여겨보라. 어쨌거나 내포 클래스는 둘레 클래스가 정의한 한 유형이며 내포 클래스의 실체는 둘레 클래스의 멤버이다. 그러므로 둘레 클래스의 모든 멤버에 접근할 수 있다. 다음은 이 원리를 보여주는 예이다. 예제는 컴파일되지 않는다. Extern
클래스의 멤버가 Outer
의 비공개 멤버에 접근하지 못하기 때문이다. 그러나 Outer::Inner
의 멤버는 Outer
의 비공개 멤버에 접근할 수 있다.
class Outer { int d_value; static int s_value; public: Outer() : d_value(12) {} class Inner { public: Inner() { cout << "Outer's static value: " << s_value << '\n'; } Inner(Outer &outer) { cout << "Outer's value: " << outer.d_value << '\n'; } }; }; class Extern // 컴파일되지 않는다! { public: Extern(Outer &outer) { cout << "Outer's value: " << outer.d_value << '\n'; } Extern() { cout << "Outer's static value: " << Outer::s_value << '\n'; } }; int Outer::s_value = 123; int main() { Outer outer; Outer::Inner in1; Outer::Inner in2(outer); }
이제 Surround
클래스에 FirstWithin
과 SecondWithin
두 개의 클래스가 있다고 생각해 보자. 세 클래스는 각각 정적 int s_variable
멤버가 있다.
class Surround { static int s_variable; public: class FirstWithin { static int s_variable; public: int value(); }; int value(); private: class SecondWithin { static int s_variable; public: int value(); }; };
Surround
클래스가 FirstWithin
과 SecondWithin
의 비밀 멤버에 접근해야 한다면 이 두 개의 클래스는 Surround
를 친구로 선언해야 한다. 그러면 Surround::value
함수는 내포 클래스의 비밀 멤버에 접근할 수 있다. 예를 들어 (내포 클래스에 선언된 friend
키워드를 눈여겨보라):
class Surround { static int s_variable; public: class FirstWithin { friend class Surround; static int s_variable; public: int value(); }; int value(); private: class SecondWithin { friend class Surround; static int s_variable; public: int value(); }; }; inline int Surround::FirstWithin::value() { FirstWithin::s_variable = SecondWithin::s_variable; return (s_variable); }친구로 간주될 객체의 정의 다음에 친구를 선언한다. 그래서 클래스를 정의한 다음에 친구로 선언할 수 있다. 이 경우 인-클래스 코드는 이미 앞으로 나올 클래스가 친구로 선언할 것이라는 사실을 이용할 수 있다. 예를 들면
Surround::FirstWithin::value
함수의 인-클래스 구현을 살펴보자. 필수 friend
선언을 value
함수의 구현 다음에 삽입해도 된다.
class Surround { public: class FirstWithin { static int s_variable; public: int value(); { FirstWithin::s_variable = SecondWithin::s_variable; return s_variable; } friend class Surround; }; private: class SecondWithin { friend class Surround; static int s_variable; }; };
안쪽 클래스와 바깥쪽 클래스에서 이름이 동일한 (`s_variable
') 멤버는 아래에 기술하듯이 적절한 영역 결정 표현식을 사용하여 접근할 수 있다.
class Surround { static int s_variable; public: class FirstWithin { friend class Surround; static int s_variable; // 이름이 같다. public: int value(); }; int value(); private: class SecondWithin { friend class Surround; static int s_variable; // 이름이 같다. public: int value(); }; static void classMember(); }; inline int Surround::value() { // 영역 결정 표현식 FirstWithin::s_variable = SecondWithin::s_variable; return s_variable; } inline int Surround::FirstWithin::value() { Surround::s_variable = 4; // 영역 결정 표현식 Surround::classMember(); return s_variable; } inline int Surround::SecondWithin::value() { Surround::s_variable = 40; // 영역 결정 표현식 return s_variable; }
내포 클래스는 자동으로 서로의 친구가 되지는 않는다. friend
선언을 해야 비로서 서로의 비밀 멤버에 접근할 수 있다.
FirstWithin
이 SecondWithin
의 비밀 멤버에 접근하려면 friend
선언이 SecondWithin
에 있어야 한다.
마찬가지로 FirstWithin
클래스는 그냥 friend class SecondWithin
를 사용하여 SecondWithin
클래스가 FirstWithin
클래스의 비밀 멤버에 접근하는 것을 허용한다. 컴파일러가 아직 SecondWithin
를 못 보았지만 친구 선언도 전방 선언으로 간주된다.
그러나 SecondWithin
이 FirstWithin
의 비밀 멤버에 접근할 수 있도록 권한을 부여하려면 FirstWithin
은 단순히 friend class SecondWithin
을 사용하는 정도로는 부족하다. SecondWithin
의 정의가 아직 알려져 있지 않기 때문이다.
이제 SecondWithin
의 전방 선언이 필요하다. 이 전방 선언은 Surround
클래스가 제공해야 한다. FirstWithin
클래스가 제공하는 것이 아니다. FirstWithin
클래스 자체에서 `class SecondWithin;
'와 같이 전방 선언을 지정하는 것은 의미가 없다. 이렇게 하면 외부의 (전역) SecondWithin
클래스를 참조하기 때문이다.
SecondWithin
의 전방 선언은 FirstWithin
안에서 `class Surround::SecondWithin;
'와 같이 지정할 수 없음을 주목하자. 이 시도는 다음과 같은 에러 메시지를 만들어 낼 것이다.
`Surround' does not have a nested type named `SecondWithin'
`Surround'에 이름이 `SecondWithin'인 유형이 내포되어 있지 않습니다
내포 클래스 안에서 SecondWithin
에 대하여 전방 선언을 하는 대신에 FirstWithin
클래스가 정의되기 전에 먼저 SecondWithin
클래스를 Surround
클래스가 선언해야 한다. 이런 식으로 SecondWithin
의 친구 선언을 하는 것은 FirstWithin
안에서 허용된다.
내포된 SecondWithin
클래스 말고도 바깥 수준에 SecondWithin
클래스가 존재한다고 가정하자. 그 클래스를 FirstWithin
클래스의 친구로 선언하려면 class FirstWithin
안에 friend ::SecondWithin
를 선언하라. 그 경우 바깥 수준에서 FirstWithin
의 클래스 선언을 먼저 해야 컴파일러가 friend::SecondWithin
선언을 만날 수 있다.
다음은 관련된 모든 클래스의 모든 비밀 멤버에 모든 클래스가 완전히 접근하는 예이다.
class SecondWithin; class Surround { // class SecondWithin; 필수가 아니다 (에러도 아님): // 친구 선언 (아래 참고)도 // 전방 선언이다. static int s_variable; public: class FirstWithin { friend class Surround; friend class SecondWithin; friend class ::SecondWithin; static int s_variable; public: int value(); }; int value(); // 구현은 위에 있음. private: class SecondWithin { friend class Surround; friend class FirstWithin; static int s_variable; public: int value(); }; }; inline int Surround::FirstWithin::value() { Surround::s_variable = SecondWithin::s_variable; return s_variable; } inline int Surround::SecondWithin::value() { Surround::s_variable = FirstWithin::s_variable; return s_variable; }
ios
클래스에서 ios::beg
와 ios::cur
같은 값들을 본 바가 있다. 현재의 Gnu C++ 구현에서 이 값들은 seek_dir
열거체의 값으로 정의되어 있다.
class ios: public _ios_fields { public: enum seek_dir { beg, cur, end }; };예를 들어
DataStructure
클래스는 앞뒤로 순회가 가능한 데이터 구조를 나타낸다고 간주하자. 이 클래스는 Traversal
열거체를 정의하여 FORWARD
값과 BACKWARD
값을 가질 수 있다. 게다가 setTraversal
멤버 함수를 정의하여 Traversal
유형의 인자를 요구할 수 있다. 대략 다음과 같이 정의할 수 있다.
class DataStructure { public: enum Traversal { FORWARD, BACKWARD }; setTraversal(Traversal mode); private: Traversal d_mode; };
DataStructure
클래스 안에서 Traversal
열거체의 값을 직접 사용할 수 있다. 예를 들어:
void DataStructure::setTraversal(Traversal mode) { d_mode = mode; switch (d_mode) { FORWARD: // ... 일을 한다. break; BACKWARD: // ... 다른 일을 한다. break; } }
DataStructure
클래스의 밖에서 열거체 유형의 이름은 열거체의 값을 참조하는데 사용되지 않는다. 여기에서는 클래스 이름이면 충분하다. 열거체의 값이 요구되는 경우에만 열거체 유형의 이름이 필요하다. 다음 코드에 이를 보여준다.
void fun() { DataStructure::Traversal // 열거 유형이름 필요 localMode = DataStructure::FORWARD; // 열거 유형이름 불필요 DataStructure ds; // 열거 유형이름 불필요 ds.setTraversal(DataStructure::BACKWARD); }위의 예제에서
DataStructure
클래스에 정의된 열거체의 값으로 DataStructure::FORWARD
상수를 지정했다. DataStructure::FORWARD
대신에 ds.FORWARD
구조도 허용된다. 이 구문적 방종은 보기에 흉하다고 필자는 본다. FORWARD
는 클래스 수준에 정의된 심볼 값이다. ds
의 멤버가 아니다. 멤버 선택 연산자를 사용했기 때문에 오해를 부른다.
DataStructure
클래스에 Nested
내포 클래스가 정의되어 있고 이어서 Traversal
열거체가 정의되어 있을 경우에만 두 클래스의 영역이 요구된다. 그 경우 후자의 예제는 다음과 같이 코딩했어야 한다.
void fun() { DataStructure::Nested::Traversal localMode = DataStructure::Nested::FORWARD; DataStructure ds; ds.setTraversal(DataStructure::Nested::BACKWARD); }여기에서
DataStructure::Nested::Traversal localMode = ds.Nested::FORWARD
구조를 사용할 수도 있었다. 필자는 그것을 피하고 싶었다. FORWARD
는 ds
의 멤버가 아니라 DataStructure
에 정의된 심볼이기 때문이다.
std::bad_cast
유형을 소개했다. dynamic_cast<>
연산자는 바탕 클래스 실체의 참조를 파생 클래스 참조로 변경할 수 없을 때 bad_cast
를 던진다. bad_cast
는 값에 상관없이 유형으로 나포할 수 있다.
유형은 연관 값 없이 정의할 수 있다. 빈 열거체를 정의할 수 있다. 값이 정의되지 않은 enum
이다. 빈 열거체의 유형 이름은 그 이후 예를 들어 catch
절 같은 곳에서 합법적인 유형으로 사용된다.
예제는 빈 열거체를 어떻게 정의하는지 보여준다. 반드시는 아니지만 class
안에 자주 정의된다. 그리고 어떻게 예외로 던지고 잡을 수 있는지 보여준다.
#include <iostream> enum EmptyEnum {}; int main() try { throw EmptyEnum(); } catch (EmptyEnum) { std::cout << "Caught empty enum\n"; }
Base
클래스를 추상 바탕 클래스로 정의했다. 컨테이너에 담긴 Base
클래스 포인터를 마치 벡터처럼 관리하도록 Clonable
클래스를 정의했다.
Base
클래스는 구현이 거의 필요 없는 초소형 클래스이므로 Clonable
에 내포 클래스로 정의하기에 딱 좋다. 이렇게 하면 Clonable
과 Base
사이의 밀접한 관계가 명확하게 드러난다. Base
를 Clonable
아래에 내포시켜 다음을
class Derived: public Base다음과 같이 바꾼다.
class Derived: public Clonable::Base
Base
를 내포 클래스로 정의하고 Base
가 아니라 Clonable::Base
로부터 파생시키는 것 말고는 (즉, Base
멤버에 적절하게 Clonable::
이름공간을 앞에다 붙여서 완전히 자격을 갖춘 이름을 완성하는 것 말고는) 더 이상 변경할 필요가 없다. 다음은 앞서 보여준 프로그램에서 변경한 부분이다 (14.12절). 이제 Clonable
에 내포된 Base
를 사용한다.
// 복제 가능한 내포된 Base, 인라인 멤버 포함: class Clonable { public: class Base; private: Base *d_bp; public: class Base { public: virtual ~Base(); Base *clone() const; private: virtual Base *newCopy() const = 0; }; Clonable(); explicit Clonable(Base *base); ~Clonable(); Clonable(Clonable const &other); Clonable(Clonable &&tmp); Clonable &operator=(Clonable const &other); Clonable &operator=(Clonable &&tmp); Base &base() const; }; inline Clonable::Base *Clonable::Base::clone() const { return newCopy(); } inline Clonable::Base &Clonable::base() const { return *d_bp; } // 파생 멤버와 그의 인라인 멤버: class Derived1: public Clonable::Base { public: ~Derived1(); private: virtual Clonable::Base *newCopy() const; }; inline Clonable::Base *Derived1::newCopy() const { return new Derived1(*this); } // 인라인으로 구현이 안된 멤버들: Clonable::Base::~Base() {}