제 7 장: 클래스

C 언어는 다양한 유형의 데이터를 구성하는 방법이 두 가지가 있다. C 구조체(struct)는 다양한 유형의 데이터 멤버를 보유하며 C 공용체(union)도 역시 다양한 유형의 데이터 멤버를 정의한다. 그렇지만 공용체의 데이터 멤버는 모두 메모리에 같은 위치를 차지하며 프로그래머는 어느 것을 사용해야 할지 결정해야 한다.

이 장은 클래스를 소개한다. 클래스(class)는 일종의 구조체이지만 그의 내용은 기본적으로 바깥 세계에서 접근할 수 없다. 반면에 C++ 구조체의 내용은 기본적으로 바깥 세계에서 접근할 수 있다. C++ 구조체는 별로 사용할 곳이 없다. 주로 클래스의 문맥 안에서 데이터를 모으거나 반환 값을 정교하게 정의하는 데 사용될 뿐이다. C++ 구조체에는 그저 평범한 구형 데이터만 담긴다 (POD에 관해서는 9.9절 참고). C++에서 클래스는 핵심적인 데이터 구성 도구이다. 오늘날 소프트웨어 개발은 기본적으로 데이터 은닉(data hiding)캡슐화(encapsulation)라는 두 가지 핵심 개념이 필수적이다 (3.2.1항7.1.1항).

공용체는 C++가 제공하는 또다른 데이터 구성 도구이다. 전통적인 C 공용체도 여전히 사용할 수 있지만 C++제한없는 공용체를 제공한다. 무제한 공용체는 클래스 유형을 데이터 멤버로 가질 수 있다. C++ 주해서는 C++의 다른 여러 새로운 개념을 소개한 후에 무제한 공용체를 12.6절에 다룬다.

C++C의 구조체와 공용체의 개념을 확장했다. 이 유형 안에 (이 장에 소개한) 멤버 함수를 정의할 수 있다. 멤버 함수란 이런 데이터 유형의 영역 안에서만 사용할 수 있는 함수이다. 특별한 멤버 함수도 있다. 객체가 탄생할 때 생성자(constructor)가 또는 사망할 때 소멸자(destructor)가 언제나 자동으로 호출된다. 이런 저런 멤버 함수와 더불어 그의 설계와 구성 그리고 그 뒤에 숨은 철학과 클래스를 이 장에 소개한다.

단계별로 Person 클래스를 구성해 보겠다. 데이터베이스 어플리케이션에서 이름과 주소 그리고 전화번호를 저장하는 데 사용할 수 있을 것이다.

곧바로 class Person을 만들어 보자. 무엇보다도 먼저 클래스 인터페이스와 그의 구현을 구별하는 것이 중요하다. 클래스를 느슨하게 정의하면 `데이터 집합과 그를 처리할 함수의 집합'이다. 이 정의는 나중에 더 정밀하게 재정의하겠지만 지금 당장은 이 정도면 시작하기에 충분하다.

클래스 인터페이스는 클래스를 구성하는 객체들의 조직을 선언한다. 변수를 선언하면 그 결과로 메모리가 확보된다. 예를 들어 int variable을 선언하면 컴파일러는 variable의 값을 저장할 메모리를 확보해 준다. 그러나 클래스는 선언하더라도 메모리가 전혀 확보되지 않는다. 클래스 선언에는 한 가지 선언 규칙이 따른다. C++에서 객체는 한 번만 선언할 수 있다. 그러므로 클래스 선언이라는 문구로는 메모리가 예약되어 있다는 암시가 없기 때문에 대신에 클래스 인터페이스라는 용어를 더 많이 사용한다.

클래스 인터페이스는 person.h와 같은 헤더 파일에 담는 것이 보통이다. 지금부터 class Person 인터페이스를 시작해 보자 (멤버 함수 뒤에 붙은 const 키워드에 관한 설명은 7.7절을 참고하라):

    #include <string>

    class Person
    {
        std::string d_name;         // 이름
        std::string d_address;      // 주소
        std::string d_phone;        // 전화번호
        size_t      d_mass;         // kg 단위 몸무게.

        public:                     // 멤버 함수
            void setName(std::string const &name);
            void setAddress(std::string const &address);
            void setPhone(std::string const &phone);
            void setMass(size_t mass);

            std::string const &name()    const;
            std::string const &address() const;
            std::string const &phone()   const;
            size_t mass()                const;
    };

인터페이스에 선언된 멤버 함수는 여전히 구현이 남아 있다. 이 멤버 함수들의 구현을 정의라고 부른다.

멤버 함수 외에도 클래스에는 멤버 함수가 조작할 데이터가 정의된다. 이런 데이터를 데이터 멤버라고 부른다. Person에서 데이터 멤버는 d_named_address 그리고 d_phoned_mass이다. 데이터 멤버는 비밀 접근 권한을 부여해야 한다. 클래스는 기본으로 비밀 접근 권한을 사용하기 때문에 그냥 인터페이스의 상단에 쭈욱 나열하기만 하면 된다.

바깥 세계와의 모든 통신은 멤버 함수를 통하여 이루어진다. 테이터 멤버는 새 값을 받을 수 있다 (setName 사용). 또는 검사를 위해 열람도 가능하다 (name 사용). 함수가 호출자에게 그냥 객체 안에 저장된 값을 돌려줄 뿐, 내부에 저장된 값을 변경하도록 허용하지 않으면 이런 함수를 접근자(accessors)라고 부른다.

구문적으로 클래스와 구조체 사이에는 근소한 차이만 있을 뿐이다. 클래스는 기본으로 비밀 멤버를 정의하고 구조체는 공개 멤버를 정의한다. 그렇지만 개념적으로 차이가 있다. C++에서 구조체는 C에서 사용하는 방식과 마찬가지로 사용된다. 데이터를 모으고 거기에 누구든지 자유롭게 접근할 수 있다. 반면에 클래스는 바깥 세계에서 접근하지 못하게 데이터를 감춘다 (이를 데이터 은닉(data hiding)이라고 부른다). 그리고 바깥 세계와 데이터 멤버 사이에 통신을 정의하는 멤버 함수를 제공한다.

Lakos (Lakos, J., 2001) Large-Scale C++ Software Design (Addison-Wesley)를 따라 필자는 다음과 같이 단계적으로 클래스 인터페이스를 설정하기를 제안한다.

스타일 관례는 시간이 오래 걸려서야 정착된다. 그렇지만 거기에 의무 조항은 없다. 위에 제시한 스타일 관례를 따르지 않는 것이 좋다고 느낀 독자는 자신만의 스타일을 사용해도 좋다. 그러나 제시하는 스타일 관례를 따르기를 권고한다.

마지막으로, 3.1.2항으로 되돌아가 예제 코드는 대부분 다음과 같이 사용해야 한다.

    using namespace std;
7.11절7.11.1항에 설명했듯이 using 지시어는 헤더 파일을 포함하여 전처리기 지시어 다음에 와야 한다. 다음과 같이 설정해야 한다.
    #include <iostream>
    #include "person.h"

    using namespace std;

    int main()
    {
        ...
    }

7.1: 생성자

C++ 클래스는 특별한 멤버 함수가 두 가지 있다. 클래스가 제대로 작동하려면 꼭 필요하다. 그 두 멤버 함수는 바로 생성자와 소멸자이다. 소멸자의 기본 임무는 객체가 `영역을 벗어나면' 배당되었던 메모리를 공용 풀에 반납하는 것이다. 메모리 배당에 관한 연구는 제 9장에서 다루었고 그러므로 소멸자는 거기에 더 자세하게 다룬다. 이 장은 클래스의 조직과 생성에 초점을 둔다.

생성자의 이름은 클래스 이름과 똑 같다. 생성자는 반환 값이 없으며 심지어 void조차 아니다. 예를 들어, Person 클래스는 생성자를 Person::Person()으로 정의한다. C++ 실행 시간 시스템은 클래스의 변수를 정의하는 즉시 그 클래스의 생성자가 호출되도록 보장한다. 생성자가 전혀 없는 클래스를 정의할 수 있다. 그러면 컴파일러가 기본 생성자를 대신 정의해 준다. 클래스의 실체가 정의될 때 호출된다. 이 경우 실제로 무슨 일이 일어날지는 클래스가 정의한 데이터 멤버에 달려 있다 (7.3.1항).

객체는 지역적으로 또는 전역적으로 정의할 수 있다. 그렇지만 C++에서 대부분의 객체는 지역적으로 정의된다. 전역적으로 정의된 객체는 거의 필요하지 않으며 꺼리는 것이 좋다.

객체가 지역적으로 정의되면 그의 함수가 호출될 때마다 생성자가 호출된다. 객체의 생성자는 객체가 정의되는 시점에 활성화된다 (객체는 표현식 안에서 임시 변수로 묵시적으로 정의될 수도 있다.).

객체를 정적으로 정의하면 생성자는 프로그램이 시작될 때 활성화된다. 심지어 main 함수가 시작하기도 전에 생성자가 호출된다. 예를 들어:

    #include <iostream>
    using namespace std;

    class Demo
    {
        public:
            Demo();
    };

    Demo::Demo()
    {
        cout << "Demo constructor called\n";
    }

    Demo d;       // 정의하는 즉시 실행된다.

    int main()
    {}

    /*
        출력:
        Demo constructor called
    */

프로그램 안에 Demo 클래스의 전역 실체가 하나 있고 main은 몸체가 비어 있다. 그럼에도 프로그램은 출력이 있는데 전역적으로 정의된 Demo 실체의 생성자가 출력한 것이다.

생성자는 아주 중요하고 정의가 잘 된 역할이 있다. 일단 객체가 생성되고 나면 생성자는 반드시 클래스의 모든 데이터 멤버가 합리적인 또는 적어도 정의가 잘 된 값을 가졌는지 확인해야 한다. 잠시 후에 이 중요한 과업을 다시 다루어 보겠다. 기본 생성자는 인자가 없다. 인자는 컴파일러가 정의한다. 또다른 생성자가 정의되어 있지 않은 한 그리고 그의 정의가 억제되지 않는 한 말이다 (7.6절). 또다른 생성자 말고도 필요하면 기본 생성자도 명시적으로 정의해야 한다. C++는 그렇게 하기 위한 특별한 구문도 역시 제공한다. 이에 관한 것은 7.6절에도 다룬다.

7.1.1: 첫 어플리케이션

우리의 예제 Person 클래스는 문자열 데이터 멤버가 세 개 있고 size_t d_mass 데이터 멤버가 하나 있다. 이 데이터 멤버에 접근하려면 인터페이스 함수의 통제를 받는다.

객체를 정의할 때마다 클래스의 생성자는 자신의 데이터에 `합리적인' 값이 부여되어 있는지 확인한다. 그래서 객체는 초기화되지 않은 값 때문에 고생하는 법이 없다. 데이터 멤버는 새 값이 주어질 수도 있지만 그것은 직접적으로 허용되지 않는다. 자신의 데이터 멤버를 비밀로 만드는 것(데이터 은닉)은 좋은 클래스 설계 핵심 원리이다. 그러므로 데이터 멤버를 수정하는 것은 온전히 멤버 함수가 통제하며 그래서 간접적으로 클래스 설계자에 의하여 제어된다. 클래스는 자신의 데이터에 작동하는 모든 행위를 캡슐화해 넣고 있으며캡슐화 덕분에 클래스 객체는 자신의 데이터 정합성에 `책임'이 있다고 볼 수 있다. 다음은 최소한으로 Person의 조작 멤버를 정의한 것이다.

    #include "person.h"                 // 앞에 주어짐
    using namespace std;

    void Person::setName(string const &name)
    {
        d_name = name;
    }
    void Person::setAddress(string const &address)
    {
        d_address = address;
    }
    void Person::setPhone(string const &phone)
    {
        d_phone = phone;
    }
    void Person::setMass(size_t mass)
    {
        d_mass = mass;
    }

최소한으로 정의를 했다. 아무 점검도 수행하지 않는다. 그러나 점검을 구현하는 것은 별로 어렵지 않다. 전화 번호에 숫자만 담아야 한다면 다음과 같이 정의할 수 있다.

    void Person::setPhone(string const &phone)
    {
        if (phone.find_first_not_of("0123456789") == string::npos)
            d_phone = phone;
        else if (phone.empty())
            d_phone = " - not available -";
        else
            cout << "A phone number may only contain digits\n";
    }
비슷하게 접근자 멤버를 캡슐화해 데이터 멤버에 접근하는 것을 제어한다. 접근자는 데이터 멤버가 난잡한 변경 때문에 고통받지 않도록 통제한다. 접근자는 개념적으로 객체의 데이터를 변경하지 않으므로 (데이터를 열람하기만 하므로) 이런 멤버 함수는 const 진위 함수로 주어진다. 이른바 상수 멤버 함수라고 부르는데 객체의 데이터를 변경하지 않음을 보장하기 때문에 수정 가능한 객체와 상수 객체에 모두 사용할 수 있다 (7.7절).

뒷문을 방지하기 위해 데이터 멤버가 접근자의 반환값 때문에 변경되지 않도록 확인해야 한다. 원시 유형의 값은 쉽게 처리할 수 있다. 값으로 변수의 사본이 반환되기 때문이다. 그러나 객체는 상당히 크므로 사본을 만들지 못하도록 객체를 참조로 반환한다. 뒷문은 데이터 멤버를 참조로 반환할 때 열린다. 다음 예제에 함수 정의가 남용되는 것을 볼 수 있다.

    string &Person::name() const
    {
        return d_name;
    }

    Person somebody;
    somebody.setName("Nemo");

    somebody.name() = "Eve";    // 이름을 변경하는 뒷문 발견!
뒷문을 막기 위해 객체는 접근자로부터 상수 참조로 반환된다. 다음은 Person의 접근자를 구현한 것이다.
    #include "person.h"                 // 앞에 주어짐
    using namespace std;

    string const &Person::name() const
    {
        return d_name;
    }
    string const &Person::address() const
    {
       return d_address;
    }
    string const &Person::phone() const
    {
       return d_phone;
    }
    size_t Person::mass() const
    {
       return d_mass;
    }

Person 클래스의 인터페이스는 클래스 설계의 시작점으로 남아 있다. 그의 멤버 함수는 Person 객체에 무엇을 요구할 수 있는지 정의한다. 결국 그의 멤버 구현은 단순히 기술적으로 Person 객체에게 자신의 일을 하도록 허용하는 것일 뿐이다.

다음 예제는 Person 클래스를 어떻게 사용하는지 보여준다. 객체는 초기화되어 printperson() 함수에 건네진다. 그러면 개인 데이터를 인쇄한다. printperson 함수의 매개변수 리스트에 있는 참조 연산자에 주목하라. 완전한 객체가 아니라 기존의 Person 객체에 대한 참조만 함수에 건넨다. printperson이 인자를 변경하지 않는다는 사실은 매개변수가 const로 선언되어 있으므로 확실하다.

    #include <iostream>
    #include "person.h"                 // 앞에 주어짐
    using namespace std;

    void printperson(Person const &p)
    {
        cout << "Name    : " << p.name()     << "\n"
                "Address : " << p.address()  << "\n"
                "Phone   : " << p.phone()    << "\n"
                "Mass  : " << p.mass()   << '\n';
    }

    int main()
    {
        Person p;

        p.setName("Linus Torvalds");
        p.setAddress("E-mail: Torvalds@cs.helsinki.fi");
        p.setPhone(" - not sure - ");
        p.setMass(75);           // 킬로그램.

        printperson(p);
    }
/*
    출력:

Name    : Linus Torvalds
Address : E-mail: Torvalds@cs.helsinki.fi
Phone   :  - not sure -
Mass  : 75

*/

7.1.2: 생성자: 인자의 유무

Person 클래스의 생성자는 지금까지 매개변수가 없었다. C++의 생성자에 매개변수 리스트를 정의할 수 있다. 인자는 객체가 정의될 때 공급된다.

Person 클래스에 대하여 생성자는 세 개의 문자열과 하나의 size_t를 기대하면 좋을 것 같다. 각각 이름과 주소 그리고 전화 번호와 몸무게를 나타낸다. 이 생성자는 다음과 같다 (그러나 7.3.1항도 참조할 것):

    Person::Person(string const &name, string const &address,
                   string const &phone, size_t mass)
    {
        d_name = name;
        d_address = address;
        d_phone = phone;
        d_mass = mass;
    }
물론 클래스 인터페이스에도 선언해야 한다.
    class Person
    {
        // 데이터 멤버 (변경 없음)

        public:
            Person(std::string const &name, std::string const &address,
                   std::string const &phone, size_t mass);

            // 나머지 클래스 인터페이스 (변경 없음)
    };
이제 이 생성자가 선언되었으므로 기본 생성자도 명시적으로 선언해야 한다. 여전히 데이터 멤버에 구체적으로 초기 값을 지정하지 않고 그냥 평범한 Person 객체를 생성하고 싶다면 말이다. 그리하여 Person 클래스는 두 개의 구성자를 지원할 것이다. 그리고 이제 생성자를 선언하는 부분은 다음과 같이 된다.
    class Person
    {
        // 데이터 멤버
        public:
            Person();
            Person(std::string const &name, std::string const &address,
                   std::string const &phone, size_t mass);

            // 추가 멤버
    };
이 경우, 기본 생성자는 별로 일을 많이 할 필요가 없다. Person 객체의 string 데이터 멤버를 초기화할 필요가 없기 때문이다. 이런 데이터 멤버들은 그 자체로 객체이기 때문에 각자의 기본 생성자에 의해서 빈 문자열로 초기화된다. 그렇지만 size_t 데이터 멤버도 있다. 이 멤버는 내장 유형의 변수이며 따라서 생성자가 없다. 그래서 자동으로 초기화되지 않는다. 그러므로 d_mass 데이터 멤버의 값을 명시적으로 초기화하지 않는 한, 그의 값은 다음과 같이 된다. 값이 0이라면 그렇게 나쁘지 않다. 그러나 데이터 멤버에 아무 값이나 할당되는 것은 좋지 않다. 그래서 기본 생성자도 할 일이 있다. 자동으로 초기화되지 않는 데이터 멤버를 합리적인 값으로 초기화해야 할 임무가 있다. 그의 구현은 다음과 같다.
    Person::Person()
    {
        d_mass = 0;
    }
인자의 유무에 따라 생성자를 사용하는 방법은 다음에 보여준다. karel 객체는 비어 있지 않은 매개변수를 정의하고 있는 생성자에 의하여 초기화된다. 반면에 anon 객체에는 기본 생성자가 사용된다. 객체를 생성할 때 생성자가 인자를 요구하면 반괄호로 인자를 둘러싸는 것이 좋다. 활괄호를 사용해도 되고 또 사용해야 하는 경우도 있다 (12.4.2항). 그러나 반괄호 대신에 활괄호를 마구 사용하면 생각지 못한 문제를 야기할 가능성이 높아진다 (7.2절). 그러므로 활괄호보다 반괄호를 사용하기를 권한다. 다음은 두개의 생성자 호출을 보여주는 예이다.
    int main()
    {
        Person karel("Karel", "Rietveldlaan 37", "542 6044", 70);
        Person anon;
    }
main이 시작할 때 두 개의 Person 객체가 정의된다. 지역 객체이기 때문에 main이 활성화되어 있는 동안만 생존한다.

다른 인자를 사용하여 Person 객체를 정의해야 한다면 상응하는 생성자를 Person의 인터페이스에 추가해야 한다. 클래스 생성자를 중복정의하는 외에도 기본 인자 값을 생성자에 제공하는 것도 가능하다. 이 기본 인자는 클래스 인터페이스에 다음과 같이 생성자 선언과 함께 지정해야 한다.

    class Person
    {
        public:
            Person(std::string const &name,
                   std::string const &address = "--unknown--",
                   std::string const &phone   = "--unknown--",
                   size_t mass = 0);

    };
보통, 생성자의 정의는 구현과 아주 비슷하게 보인다. 이것은 생성자의 매개변수가 종종 편의를 위해 정의되기 때문이다. phone 번호는 필요 없고 mass가 필요한 생성자는 기본 인자를 사용하여 정의할 수 없다. phone이 생성자의 마지막 매개변수가 아니기 때문이다. 결과적으로 매개변수 리스트에 phone 번호가 없는 특별한 생성자가 필요하다.

C++11 표준 이전에 이 상황은 보통 다음과 같이 처리했었다. 모든 생성자는 참조와 const 데이터 멤버를 초기화해야 한다. 그렇지 않으면 컴파일러는 (당연히) 불평을 한다. 나머지 멤버를 초기화하려면 (비-상수와 비-참조 멤버) 두 가지 선택이 있다.

현재 C++에서 생성자는 서로를 호출할 수 있다 (생성자 위임이라고 부름). 이것은 아래 7.4.1항에 보여준다.

7.1.2.1: 생성 순서

인자를 생성자에 건넬 수 있으면 프로그램을 실행하는 동안 객체의 생성 순서를 관제할 수 있다. 다음 프로그램에 Test 클래스를 사용하여 실제 예를 보여준다. 이 프로그램은 전역 Test 객체와 두 개의 지역 Test 객체를 정의한다. 생성 순서는 예상대로이다. 먼저 전역 개체가 생성되고 다음에 메인 함수의 첫 번째 지역 객체가 생성되며 그 다음에 func의 지역 객체가 생성된다. 그리고 마지막으로 main의 두 번째 지역 객체가 생성된다.
    #include <iostream>
    #include <string>
    using namespace std;

    class Test
    {
        public:
            Test(string const &name);   // 인자가 주어진 생성자
    };

    Test::Test(string const &name)
    {
        cout << "Test object " << name << " created" << '\n';
    }

    Test globaltest("global");

    void func()
    {
        Test functest("func");
    }

    int main()
    {
        Test first("main first");
        func();
        Test second("main second");
    }
/*
    출력:
Test object global created
Test object main first created
Test object func created
Test object main second created
*/

7.2: 모호성 해결

객체를 정의하다 보면 예상치 못한 결과에 놀랄 수도 있다. 다음 클래스 인터페이스가 있다고 간주하자:
    class Data
    {
        public:
            Data();
            Data(int one);
            Data(int one, int two);

            void display();
    };

Data 클래스의 실체를 두 개 생성하는 것이 의도이다. 각각 첫 번째와 두 번째 생성자를 사용한다. 코드는 다음과 같이 보인다 (그리고 올바르게 컴파일된다):

    #include "data.h"
    int main()
    {
        Data d1();
        Data d2(argc);
    }

이제 Data 실체를 이용할 시간이다. 두 개의 서술문을 main에 추가한다.

        d1.display();
        d2.display();
그러나 놀랍게도 컴파일러는 첫 번째 서술문에 불평을 한다.

error: request for member 'display' in 'd1', which is of non-class type 'Data()'
에러: 'd1' 유형에 'display' 멤버를 요청했습니다. 'd1'은 유형이 클래스가 아닌 'Data()'입니다.

무슨 일이 일어나고 있는가? 무엇보다도 컴파일러가 가리키고 있는 데이터 유형에 주목하라: Data()가 그것이다. Data가 아님을 유의하라. ()는 뭐하는 것인가?

이 문제에 대답하기 전에 우리의 이야기를 좀 넓혀 보자. 라이브러리 어디엔가 dataFactory공장 함수가 존재하는 것을 우리는 알고 있다. 공장 함수는 특정 유형의 객체를 만들어 돌려준다. 이 dataFactory 함수는 Data 객체를 돌려준다. Data의 기본 생성자를 사용하여 생성했다. 그러므로 dataFactory는 인자가 없다. dataFactory를 프로그램에 사용하고 싶지만 그 함수를 선언해야 한다. 그래서 그 선언을 main에 추가한다. 그곳이 dataFactory가 사용될 유일한 곳이기 때문이다. 그것은 함수이며 인자를 요구하지 않고 Data 객체를 돌려준다.

        Data dataFactory();
그렇지만 이것은 놀랍도록 우리의 d1 객체 정의와 비슷하다.
        Data d1();

문제의 근원을 찾았다. Data d1()d1 객체의 정의가 아니다. Data 객체를 돌려주는 함수의 선언이다. 그래서 여기에서 무슨 일이 일어나는가? Data의 기본 생성자를 사용하여 Data 객체를 어떻게 정의할 것인가?

첫 째: 여기에서 일어나는 일은 컴파일러가 Data d1()을 맞이하면 선택의 기로에 선다는 것이다. Data 객체를 정의하거나 아니면 함수를 선언하는 것이다. 컴파일러는 함수 선언을 선택한다.

실제로 여기에서 C++ 구문의 모호성을 만나고 있다. 이 모호성은 C++ 표준에 따라 해결되었다. 언제나 정의보다 선언을 먼저 하라. 이 모호함이 일어나는 상황을 나중에 이 절에서 더 많이 만나게 될 것이다.

둘 째: 이 모호성을 원하는 대로 해결할 수 있는 여러 가지 방법이 있다. 기본 생성자를 사용하여 객체를 정의하려면:

7.2.1: `Data' 유형 vs. `Data()'

위의 문맥에서 기본 생성된 익명 Data 객체를 정의하는 Data()는 다시 컴파일러 에러를 일으킨다. 컴파일러에 의하면 원래의 d1Data 유형이 아니라 Data() 유형이다. 그래서 그것이 무엇인가?

먼저 두 번째 생성자를 살펴 보자. int를 기대한다. 두 번째 생성자를 사용하여 또 다른 Data 객체를 정의하고 싶을 것이다. 그러나 int()를 사용하여 기본 int 값을 생성자에 건네고 싶다. 우리는 이 생성자가 기본 int 값을 정의한다는 사실을 알고 있다. cout << int() << '\n'가 멋지게 0을 화면에 보여주기 때문이다. int x = int()도 x를 0으로 초기화해 줄 것이므로 `Data di(int())'main에 정의해 본다.

좋지 않다. di를 사용하려고 시도하면 또다시 컴파일러가 불평한다. `di.display()' 후에 컴파일러는 다음과 같이 불평을 토해낸다.

error: request for member 'display' in 'di', which is of non-class type 'Data(int (*)())'
에러: 'd1' 유형에 'display' 멤버를 요청했습니다. 'd1'은 유형이 클래스가 아닌 'Data(int (*)())'입니다.

이런, 예상대로가 아니다.... 0을 건네지 않았나? 왜 갑자기 포인터가 나타나지? 또다시 `가능하면 선언을 선택하라' 전략과 똑 같다. 표기법 Type()Type 유형이 기본 값을 나타낼 뿐만 아니라 함수를 가리키는 익명 포인터를 위한 단축 표기법이기도 하다. 인자를 기대하지 않으며 Type 값을 돌려준다. `int (*ip)() = nullptr'를 정의하고 ip를 인자로 di에 건네보면 이를 확인할 수 있다. di(ip)는 깔끔하게 컴파일된다.

그래서 int()int x에 삽입하거나 할당할 때는 왜 에러가 일어나지 않는가? 이 두 사례에는 선언이 없다. 오히려 coutint x =는 값을 결정하는 표현식이 필요하다. 이 표현식은 int()로 `자연스럽게' 해석된다. 그러나 `Data di(int())'에서 컴파일러는 또다시 선택을 한다. 그리고 (설계상) 선언이 우선 순위가 높기 때문에 선언을 선택한다. 이제 익명 포인터로서 int()를 해석할 수 있고 그러므로 이것이 사용된다.

마찬가지로 int x가 선언되어 있으면 `Data b1(int(x))'b1을 함수로 선언한다. int를 기대한다 (int(x)가 유형을 나타내기 때문이다). 반면에 `Data b2((int)x)'b2Data 객체로 선언한다. int 값 하나를 기대하는 생성자를 사용한다.

역시, 기본 개체나 값 또는 객체를 사용하려면 ()말고 {}이 더 좋다. Data di{ int{} }는 유형이 Datadi를 생성하고 Data(int x) 생성자를 호출하며 그리고 int의 기본 값 0을 사용한다.

7.2.2: 과도한 괄호

좀 더 연구해 보자. 프로그램의 한 시점에서 int b를 정의했다. 그 다음에 복합 서술문에서 익명의 Data 객체를 생성할 필요가 있다. b를 사용하여 초기화한 다음에 b를 화면에 보여준다.
    int b = 18;
    {
        Data(b);
        cout << b;
    }
cout 서술문에 관하여 컴파일러는 다음과 같이 보고한다 (의미가 잘 드러나도록 에러 메시지를 좀 수정했다):

error: cannot bind `std::ostream & << Data const &'
에러: `std::ostream & << Data const &'를 묶을 수 없습니다.

int b를 삽입하지 않고 Data b를 삽입했다. 복합 서술문을 생략했다면 컴파일러는 두 번 정의된 b 개체에 대하여 불평했을 것이다. Data(b)는 그냥 Data b를 의미할 뿐이기 때문이다. 기본으로 생성된 Data 객체이다. 컴파일러는 정의나 선언을 해석할 때 과도한 괄호를 생략해 버리기도 한다.

물론, 문제는 이제 어떻게 int b로 초기화되는 Data 임시 객체를 정의할 수 있는가이다. 컴파일러가 과도한 괄호를 생략해 버릴 수 있다는 사실을 기억하라. 그래서 이름을 사용하지 않고 int를 익명의 Data 객체에 건넬 필요가 있다.

값과 유형은 큰 차이가 있다. 다음 정의를 연구해 보자:

    Data (*d4)(int);    // 1
    Data (*d5)(3);      // 2
정의 1은 문제를 야기하지 않는다. 함수를 가리키는 포인터이다. int를 기대하고 Data 객체를 돌려준다. 그래서 d4는 포인터 변수이다.

정의 2는 약간 더 복잡하다. 물론 포인터이다. 그러나 함수와 관련이 전혀 없다. 그래서 3이 들어 있는 인자 리스트는 무슨 일을 하는가? 인자 리스트가 아니다. 마치 인자 리스트처럼 보이는 초기화이다. 변수는 반괄호 또는 활괄호로 둘러싸서 할당 서술문으로 초기화할 수 있다는 사실을 기억하라. 그래서 `(3)' 대신에 `= 3' 또는 `{3}'이라고 해도 된다. 첫 번째 대안을 골라 보자. 결과는 다음과 같다.

    Data (*d5) = 3;
이제 다시 `컴파일러를 기동한다'. 과도한 반괄호를 제거하면 다음과 같다.
    Data *d5 = 3;
Data 객체를 가리키는 포인터이다. 3으로 초기화된다 (의미구조적으로 부정확하지만 구문 분석을 끝내면 바로 명확하게 드러난다. 처음부터 다음과 같이 작성했다면
     Data (*d5)(&d1);      // 2
int3을 대조하는 재미가 반감되었을 것이다.).

7.2.3: 기존의 유형

일단 유형의 이름이 정의되어 있으면 그 이름은 변수를 나타내는 식별자를 압도한다. 이 경우 컴파일러는 식별자 대신에 유형 이름을 선택한다. 이것 역시 흥미로운 생성을 야기할 수 있다.

int를 기대하는 process 함수가 라이브러리에 있다고 가정해 보자. 이 함수를 사용하여 int 데이터 값을 처리하고 싶다. 그래서 mainprocess를 선언하고 호출한다.

    int process(int Data);
    process(argc);
아무 문제가 없다. 그러나 코드를 `아름답게 고치기로' 마음을 먹고 다음과 같이 과도한 반괄호를 좀 걷어내면:
    int process(int (Data));
    process(argc);
불행하게도 이제 곤경에 빠진다. 컴파일러는 에러를 뱉아낸다. 선언이 정의보다 우선한다는 컴파일러의 규칙 때문이다. Dataclass Data의 이름이 되고 int (x)처럼 int (Data) 매개변수는 int (*)(Data)로 해석된다. 함수를 가리키는 포인터이다. Data 객체를 기대하고 int를 돌려준다.

다음은 또다른 예이다. 다음과 같이 선언하는 대신에

    int process(int Data[10]);
배열이 process에 건네진다는 사실을 강조하기 위해 다음과 같이 선언하면:
    int process(int (Data[10]));
process 함수는 int 값을 포인터로 기대하는 것이 아니라, Data 원소를 포인터로 기대하고 int를 돌려주는 함수를 포인터로 기대한다.

`모호성 결정'에 관한 절에서 발견한 사실들을 요약하면:

7.3: 객체 안의 객체: 합성

Person 클래스는 객체를 데이터 멤버로 사용한다. 이런 생성 테크닉을 합성(composition)이라고 부른다.

합성은 기이한 것도 아니고 C++만의 전유물도 아니다. C에서 다른 복합 유형에 구조체(struct)나 공용체(union) 필드가 사용된다. C++에서는 좀 특별한 생각을 요구한다. 초기화에 제한이 있는 경우가 있기 때문이다. 이를 다음 몇 개의 항에 다룬다.

7.3.1: 합성과 상수 객체: 상수 멤버 초기화

따로 지정하지 않는 한, 클래스의 실체 데이터 멤버는 기본 생성자로 초기화된다. 기본 생성자를 사용하는 것이 언제나 객체를 초기화하는 최적의 방법은 아니다. 심지어 불가능할 수도 있다. 클래스는 기본 생성자를 정의하지 않을 수도 있다.

이전에 Person에서 다음 생성자를 본 적이 있다.

    Person::Person(string const &name, string const &address,
                   string const &phone, size_t mass)
    {
        d_name = name;
        d_address = address;
        d_phone = phone;
        d_mass = mass;
    }
이 생성자에서 무슨 일이 일어나는지 잠시 생각해 보자. 생성자의 몸체에서 문자열 객체에 할당하는 것을 볼 수 있다. 할당이 생성자의 몸체에 사용되기 때문에 왼쪽에 객체가 반드시 존재해야 한다. 그러나 객체가 존재하기 시작하면 생성자가 반드시 호출되어야 한다. 그런 객체의 초기화는 그 즉시 Person의 생성자 몸체에서 무력화되어 버린다. 이것은 비효율적일 뿐만 아니라 완전히 불가능한 경우도 있다. 클래스 인터페이스에 string const 데이터 멤버가 있다고 가정해 보자: 값이 전혀 변경되지 않는 데이터 멤버이다 (예를 들어 생일은 보통 바뀌지 않으며 그러므로 string const 데이터 멤버의 훌륭한 후보이다). 생일 객체를 생성하고 거기에 최초 값을 제공하는 것은 문제가 없다. 그러나 최초 값을 변경하는 것은 그렇지 않다.

생성자의 몸체는 데이터 멤버에 할당을 허용한다. 데이터 멤버의 초기화는 그보다 먼저 일어난다. C++멤버 초기화 구문이 정의되어 있다. 이를 이용하면 데이터 멤버를 생성 시간에 초기화하는 방식을 지정할 수 있다. 멤버 초기화는 생성 지정 리스트로 지정된다. 다음과 같이 생성자의 매개변수 리스트 다음에 쌍점과 그리고 여는 반괄호 사이에 생성자를 지정한다.

    Person::Person(string const &name, string const &address,
                   string const &phone, size_t mass)
    :
        d_name(name),
        d_address(address),
        d_phone(phone),
        d_mass(mass)
    {}

예제에서 초기화 표현식을 괄호로 둘러싸 멤버를 초기화했다. 괄호 대신에 활괄호를 사용해도 된다. 예를 들어 d_name을 다음과 같이 초기화할 수도 있다.

        d_name{ name },

멤버 초기화는 언제나 클래스에서 실체를 생성할 때 일어난다. 멤버 초기화 리스트에 생성자를 전혀 언급하지 않으면 객체의 기본 생성자가 호출된다. 이것은 객체에 대해서만 유효함을 주의하라. 원시 유형의 데이터 멤버는 자동으로 초기화되지 않는다.

그렇지만 멤버 초기화는 intdouble 같은 원시 유형의 멤버에도 사용할 수 있다. 위의 예제는 mass 매개변수로부터 d_mass 데이터 멤버를 초기화하는 것을 보여준다. 멤버 초기화가 사용될 때 데이터 멤버는 심지어 생성자의 매개변수와 이름이 같을 수도 있다 (물론 이것은 권장하지 않는다). 모호함이 전혀 없고 멤버 초기화에 사용된 (왼쪽의) 첫 식별자가 언제나 초기화되는 데이터 멤버이고 반면에 반괄호 사이에 있는 식별자는 매개변수로 번역되기 때문이다.

클래스 유형의 데이터 멤버를 초기화하는 순서는 합성 클래스 인터페이스에 멤버들이 정의되어 있는 순서를 따른다. 생성자의 초기화 순서가 클래스 인터페이스의 순서와 다르면 컴파일러는 불평을 하고 클래스 인터페이스의 순서에 맞게 초기화 순서를 바꾼다.

멤버 초기화는 되도록이면 자주 사용해야 한다. 보시다시피 (예를 들어 상수 데이터 멤버를 초기화하기 위해 또는 기본 생성자가 없는 클래스의 실체를 초기화하기 위해) 멤버 초기화를 사용하지 않으면 비효율적인 코드가 만들어지기 때문에 멤버 초기화의 사용이 꼭 필요할 수도 있다. 명시적으로 멤버 초기화를 지정하지 않는 한, 데이터 멤버의 기본 생성자가 언제나 자동으로 호출되기 때문이다. 그래서 기본 생성자 다음에 생성자 몸체에서 재할당하는 것은 확실히 비효율적이다. 물론, 어떤 때는 기본 생성자를 사용해도 좋지만 그런 경우는 명시적으로 멤버 초기화를 생략할 수 있다.

제일 규칙으로서 생성자 몸체에서 값을 데이터 멤버에 할당한다면 그 할당을 피하도록 노력하라. 멤버 초기화를 사용하는 것이 더 좋다.

7.3.2: 합성과 참조 객체: 참조 객체 초기화자

합성된 객체를 (const 객체이든 아니든) 초기화하는 것 말고도 멤버 초기화를 사용해야 하는 또다른 상황이 있다. 다음 상황을 연구해 보자.

프로그램은 Configfile 클래스의 실체를 사용한다. main에 정의되어 환경구성 파일에 있는 정보에 접근한다. 환경구성 파일에 프로그램의 매개변수가 들어 있다. 명령줄 인자로 공급하는 대신에 값에 따라 환경을 바꿀 수 있다.

main에 사용된 또다른 객체가 Process 클래스의 실체라고 간주하자. 이 실체가 `모든 일을 처리한다'. 어떤 상황에 Configfile 클래스의 실체가 존재한다고 Process 클래스의 실체에게 알려 주어야 할 필요가 있는가?

그러나 참조 변수는 할당으로 초기화할 수 없다. 그래서 다음은 올바르지 않다.
    Process::Process(Configfile &conf)
    {
        d_conf = conf;        // 잘못이다. 할당 불가
    }
d_conf = conf 서술문은 실패한다. 초기화가 아니라, 한 Configfile 객체(conf)를 또다른 객체(d_conf)에 할당하기 때문이다. 참조 변수에 대한 할당은 실제로는 참조 변수가 참조하는 변수에 대한 할당이다. 그러나 d_conf가 어느 변수를 참조하는가? 전혀 변수를 참조하지 않는다. d_conf를 초기화하지 않았기 때문이다. 결국 d_conf = conf 서술문의 목적은 d_conf를 초기화하는 것이었다.

어떻게 d_conf를 초기화할까? 다시 한 번 멤버 초기화 구문을 사용한다. 다음은 d_conf를 초기화하는 올바른 방법이다.

    Process::Process(Configfile &conf)
    :
        d_conf(conf)      // 참조 멤버를 초기화한다.
    {}
위의 구문은 참조 데이터 멤버가 사용되는 모든 경우에 사용해야 한다. 예를 들어 d_irint 참조 데이터 멤버라면 다음과 같이 생성해야 한다.
    Process::Process(int &ir)
    :
        d_ir(ir)
    {}

7.4: 데이터 멤버 초기화

클래스의 비-정적 데이터 멤버는 클래스의 생성자로 초기화된다. 생성자는 다르지만 초기화가 같은 경우가 자주 있다. 결과적으로 여러 지점에서 초기화가 수행되는데 이 때문에 클래스를 유지관리하기가 복잡해진다.

여러 데이터 멤버를 정의하고 있는 클래스를 연구해 보자: 데이터를 가리키는 포인터가 있고 그 포인터가 가리키는 데이터 원소의 갯수를 저장한 데이터 멤버가 있으며 그리고 객체의 순번을 저장한 데이터 멤버가 있다. 클래스는 또한 기본 생성자 집합을 제공한다. 다음 클래스 인터페이스에 보여주는 바와 같다.

    class Container
    {
        Data *d_data;
        size_t d_size;
        size_t d_nr;

        static size_t s_nObjects;

        public:
            Container();
            Container(Container const &other);
            Container(Data *data, size_t size);
            Container(Container &&tmp);
    };
데이터 멤버의 최초 값은 쉽게 기술할 수 있지만 구현하기는 좀 어렵다. 최초 상황을 생각해 보고 기본 생성자가 사용된다고 간주하자: 모든 데이터 멤버는 0으로 설정되어야 한다. d_nr은 제외한다. ++s_nObjects 값을 부여해야 하기 때문이다. 이것들은 기본이 아닌 행위들이기 때문에 = default를 사용하여 기본 생성자를 선언할 수 없다. 오히려 실제로 구현해야 한다.
    Container()
    :
        d_data(0),
        d_size(0),
        d_nr(++s_nObjects)
    {}
사실상 모든 생성자는 d_nr(++s_nObjects) 초기화를 서술하기를 요구한다. 그래서 d_data의 유형이 (이동을 인식하는) 클래스 유형이라면 여전히 위의 구성자를 모두 구현해야 한다.

그렇지만 C++데이터 멤버 초기화도 제공한다. 비-정적 데이터 멤버를 간단하게 초기화할 수 있다. 데이터 멤버 초기화로 최초 값을 데이터 멤버에 할당할 수 있다. 컴파일러는 이 최초 값들을 초기화 표현식으로부터 계산할 수 있겠지만 최초 값은 반드시 상수 표현식이어야 할 필요가 없다. 그래서 ++s_nObjects는 최초 값이 될 수 있다.

Container 클래스에 데이터 멤버 초기화를 사용한 코드는 다음과 같다.

    class Container
    {
        Data *d_data = 0;
        size_t d_size = 0;
        size_t d_nr = ++nObjects;

        static size_t s_nObjects;

        public:
            Container() = default;
            Container(Container const &other);
            Container(Data *data, size_t size);
            Container(Container &&tmp);
    };
데이터 멤버 초기화를 컴파일러가 인식하고 기본 생성자의 구현에 적용한다는 것을 눈여겨보라. 실제로 명시적으로 초기화하지 않는 한, 모든 생성자는 데이터 멤버 초기화를 적용한다. 이동-생성자는 이제 다음과 같이 구현할 수 있다.
    Container(Container &&tmp)
    :
        d_data(tmp.d_data),
        d_size(tmp.d_size)
    {
        tmp.d_data = 0;
    }
d_nr의 초기화는 구현에서 빠졌지만 클래스 인터페이스에 제공된 데이터 멤버 초기화 덕분에 초기화된다.

집합체(aggregate)는 배열 또는 class이다 (사용자가 정의한 생성자가 없고 비밀 또는 보호된 비-정적 데이터 멤버도 없으며 바탕 클래스도 없고 (제 13장) 그리고 가상 함수도 없는 (제 14장) 구조체(struct)이다). 다음과 같이 말이다.

    struct POD      // 집합체 POD 정의
    {
        int first = 5; 
        double second = 1.28; 
        std::string hello {"hello"};
    };
C++14 표준에서는 활괄호 초기화 리스트를 사용하여 집합체를 초기화할 수 있다. 예를 들어 다음과 같이
    POD pod {4, 13.5, "hi there"};
활괄호 초기화 리스트를 사용할 때 모든 데이터 멤버를 초기화할 필요가 있는 것은 아니다. 아무 데이터 멤버에서나 지정을 멈출 수 있다. 그 경우 나머지 데이터 멤버는 기본 값이 또는 명시적으로 정의된 초기화 값이 사용된다. 예를 들어,
    POD pod {4};    // 나머지는 다음과 같이 second: 1.28, hello: "hello"를 사용한다.

7.4.1: 생성자 위임하기

생성자는 서로 특정화의 관계에 있는 경우가 가끔 있다. 그의 모든 데이터 멤버에 인자의 부집합만 지정하고 나머지 데이터 멤버에는 기본 값을 할당해 객체를 생성할수 있다.

C++11 표준 이전에는 생성자들에게 공통적으로 모든 초기화를 수행하는 init같은 멤버를 정의하는 것이 흔한 관행이었다. 그렇지만 그런 init 함수는 const나 참조 데이터 멤버 초기화에 사용할 수 없다. 이른바 바탕 클래스 초기화에도 사용할 수 없다 (제 13장).

다음은 그런 init 함수가 사용되는 예이다. Stat 클래스는 Cstat(2) 함수를 둘러싼 포장 클래스로 설계되었다. 이 클래스는 세 개의 생성자를 정의한다. 첫 번째는 인자를 기대하지 않으며 모든 데이터 멤버를 적절한 값으로 초기화한다. 두 번째도 같은 일을 하지만 생성자에게 건넨 파일 이름에 stat 함수를 호출한다. 그리고 세 번째는 파일이름과 검색 경로를 기대한다. 생성자마다 초기화 코드를 반복하는 대신에 공통 코드를 init 멤버 안에 넣어서 생성자가 호출하도록 만든다.

현재, C++는 대안을 제공한다. 생성자끼리 서로 호출할 수 있도록 허용한다. 이것을 생성자 위임이라고 부른다. C++11 표준은 다음 예제에 보여주는 바와 같이 생성자를 위임할 수 있다.

    class Stat
    {
        public:
            Stat()
            :
                State("", "")   // 파일이름/검색경로 없음
            {}
            Stat(std::string const &fileName)
            :
                Stat(fileName, "")  // 파일이름만 제공
            {}
            Stat(std::string const &fileName, std::string const &searchPath)
            :
                d_filename(fileName),
                d_searchPath(searchPath)
            {
                // 나머지 조치는 생성자가 수행한다.
            }
    };

C++는 정수 유형 데이터 멤버가 정적 상수라면 클래스 인터페이스 안에서 초기화하도록 허용한다 (제 8장). C++11 표준에는 여기에다 클래스 인터페이스의 평범한 데이터 멤버에 대하여 기본 초기화를 정의하는 편의기능도 추가되었다 (이 데이터 멤버들은 const나 정수 유형일 수도 아닐 수도 있다. 그러나 (물론) 참조 데이터 멤버일 수는 없다).

이 기본 초기화는 생성자가 통제할 수 있다. 예를 들어 Stat 클래스에 기본 값이 falsebool d_hasPath 데이터 멤버가 있는데 세 번째 생성자가 그것을 true로 초기화해야 한다면 (위 참고) 다음 접근법이 가능하다.

    class Stat
    {
        bool d_hasPath = false;

        public:
            Stat(std::string const &fileName, std::string const &searchPath)
            :
                d_hasPath(true)     // 인터페이스에 지정된 값을
                                    // 통제한다.
            {}
    };
d_hasPath는 값을 한 번만 받는다. 언제나 false로 초기화된다. 단, 위에 보여준 생성자가 사용되어 true로 바뀌는 경우는 제외한다.

7.5: 통일 초기화

변수와 객체를 정의할 때 즉시 초기 값을 줄 수 있다. 클래스 유형의 객체는 언제나 자신의 생성자 중 하나를 사용하여 초기화된다. C는 이미 배열과 구조체에 초기화 리스트를 지원한다. 상수 표현식 객체에 한 쌍의 활괄호를 둘러 구성한다.

C++에도 비견되는 초기화가 있다. 통일 초기화(uniform initialization)라고 부르는데 다음 구문을 사용한다.

    Type object {value list};
객체 리스트를 사용하여 객체를 정의하면 객체마다 자신만의 통일 초기화를 사용할 수 있다.

생성자를 사용하는 것에 비해 통일 초기화의 장점은 모호성이 생기지 않는다는 것이다. 생성자를 사용하면 모호성이 생기는데, 객체를 생성하는 것이 종종 객체의 중복정의 함수 호출 연산자를 사용하는 것과 혼동되기 때문이다 (11.10절). 초기화 리스트는 평범한 구형 데이터 (POD) 유형에만 사용할 수 있기 때문에 (9.9절) 그리고 (std::vector와 같이) `초기화 리스트를 인지'하는 유형에만 사용할수 있기 때문에 초기화 리스트를 사용하면 모호성은 일어나지 않는다.

통일 초기화를 사용하면 객체나 변수를 초기화할 수 있다. 뿐만 아니라 생성자에 있는 또는 함수의 반환 서술문에 있는 데이터 멤버를 묵시적으로 초기화할 수도 있다. 예를 들어:

    class Person
    {
        // 데이터 멤버
        public:
            Person(std::string const &name, size_t mass)
            :
                d_name {name},
                d_mass {mass}
            {}

            Person copy() const
            {
                return {d_name, d_mass};
            }
    };

객체 정의는 의외의 곳에서도 만날 수 있다. `func' 함수가 있고 아주 간단한 Fun 클래스가 있다고 생각하자 (struct가 사용된다. 데이터 은닉은 여기에서 핵심이 아니다. 간략하게 하기 위해 클래스 안에 구현한다):

    void func();

    struct Fun
    {
        Fun(void (*f)())
        {
            std::cout << "Constructor\n";
        };

        void process()
        {
            std::cout << "process\n";
        }
    };
mainFun fun(func)과 같이 Fun 객체가 정의된다. 이 프로그램을 실행하면 Constructor가 화면에 나타난다. fun이 생성된다. 다음으로 익명의 Fun 객체에 process를 호출할 생각이다.
    Fun fun(func);
    Fun(func).process();
Constructor가 다시 또 나타난다. 그리고 process가 화면에 보여진다.

그냥 익명 Fun 객체를 정의하면 어떨까? 이렇게 말이다.

    Fun(func);
이제 놀라운 광경이 펼쳐진다. 컴파일러는 Fun의 기본 생성자가 빠졌다고 불평한다. 왜 그런가? Fun 다음에 빈 공백을 몇 개 넣어 보라. 그러면 Fun (func)을 얻는다. 식별자 둘레에 있는 괄호는 문제가 없다. 반괄호 처리된 표현식을 해석하고 난 뒤에는 걷어내도 된다. 이 경우에 (func)func와 동일하므로 Fun func를 얻는다. Fun func 객체의 정의이고 Fun의 기본 생성자를 사용한다 (이것은 제공되지 않는다).

그런데 왜 Fun(func).process()는 컴파일되는가? 이 경우 멤버 선택 연산자가 있기 때문이다. 왼쪽의 피연산자는 반드시 클래스 유형의 객체이어야 한다. 그런 객체가 존재해야 하고 Fun(func)는 그 객체를 나타낸다. 이미 있는 객체의 이름이 아니다. 그러나 func 같은 함수를 기대하는 생성자가 존재한다. 이제 컴파일러는 익명의 Fun을 생성해 func에 인자로 건넨다.

이 예제에서 익명의 Fun 객체를 생성하기 위해 반괄호를 사용할 수 없는 것은 확실하다. 그렇지만 통일 초기화를 사용할 수 있다. 익명의 Fun 객체를 정의하려면 다음 구문을 사용하라:

    Fun {func};
(멤버 중 하나를 즉시 호출하는 데에도 사용할 수 있다. 예를 들어 Fun{func}.process()).

통일 초기화 구문은 (할당 연산자를 사용하는) 초기화 리스트의 구문과 약간 다르지만 그럼에도 컴파일러는 생성자가 초기화 리스트를 지원하면 그것을 사용한다. 예제로 다음을 연구해 보자:

    class Vector
    {
        public:
            Vector(size_t size);
            Vector(std::initializer_list<int> const &values);
    };

    Vector vi = {4};
vi를 정의할 때 초기화 리스트를 기대하는 생성자가 호출된다. size_t 인자를 기대하는 생성자를 호출하려면 표준 생성자 구문으로 정의해야 한다. 즉, Vector vi(4)를 사용해야 한다.

초기화 리스트는 그 자체가 또다른 초기화 리스트를 사용하여 생성될 수 있는 객체이다. 그렇지만 초기화 리스트에 저장된 값들은 변경이 불가능하다. 일단 초기화 리스트가 정의되면 그의 값들은 변하지 않는다.

initializer_list를 사용하기 전에 initializer_list 헤더를 포함해야 한다.

초기화 리스트는 다음과 같은 기본 멤버 함수와 생성자들을 지원한다.

7.6: default 키워드와 delete 키워드

클래스를 설계하다 보면 두 가지 상황을 자주 맞이한다. 클래스에 생성자가 하나라도 정의되어 있으면 컴파일러는 기본 생성자를 자동으로 정의하지 않는다. C++는 그 제한을 약간 풀었다. `= default' 구문을 제공한다. `= default'를 기본 생성자 구문으로 지정한 클래스는 컴파일러가 기본 생성자를 제공해야 한다고 알린다. 기본 생성자는 다음과 같은 일을 한다. 복사 생성자중복정의 할당 연산자 그리고 소멸자에 대해서도 역시 기본 구현을 제공한다. 이 멤버들은 제 9장에 소개한다.

반대로 어떤 멤버는 사용할 수 있으면 안되는 경우가 있다 (그렇지 않으면 자동으로 제공된다). 이것은 `= delete'를 지정하여 실현한다. = default와 그의 사용법은 다음 예제에 보여준다. 기본 생성자는 기본 구현을 받는다. 복사-생성은 금지된다.

    class Strings
    {
        public:
            Strings() = default;
            Strings(std::string const *sp, size_t size);

            Strings(Strings const &other) = delete;
    };

7.7: const 멤버 함수와 상수 객체

const 키워드는 종종 멤버 함수의 매개변수 리스트 뒤에 사용된다. 이 키워드는 멤버 함수가 객체의 데이터 멤버를 변경하지 않는다는 사실을 나타낸다. 그런 멤버 함수를 상수 멤버 함수라고 부른다. Person 클래스에서 접근자 함수를 const로 선언했었다.
    class Person
    {
        public:
            std::string const &name()    const;
            std::string const &address() const;
            std::string const &phone()   const;
            size_t mass()              const;
    };
3.1.1항에 제시한 제일 규칙이 여기에도 적용된다. const 키워드의 왼쪽에 나타나는 것은 무엇이든 바뀌지 않는다. 멤버 함수에게 이것은 자신의 데이터를 `변경하지 말 것'이라는 뜻이다.

상수 멤버 함수를 구현할 때 const 속성을 반복해야 한다.

    string const &Person::name() const
    {
        return d_name;
    }
컴파일러는 상수 멤버 함수 중 하나로 클래스의 데이터 멤버가 변경되지 않도록 방지한다. 때문에 다음 서술문은
    d_name[0] = toupper(static_cast<unsigned char>(d_name[0]));
위의 함수 정의에 추가하면 컴파일 에러가 된다.

const 멤버 함수는 우발적인 데이터 변경을 방지한다. 생성자와 소멸자를 제외하고 const 객체에는 (또는 참조나 포인터에는) 상수 함수 멤버만 사용할 수 있다(제 9장).

상수 객체는 함수의 const & 매개변수로 자주 만난다. const & 함수 안에서는 상수 멤버만 사용된다. 다음은 한 예이다.

    void displayMass(ostream &out, Person const &person)
    {
        out << person.name() << " weighs " << person.mass() << " kg.\n";
    }
personPerson const &으로 정의되어 있으므로 displayMass 함수는 person.setMass(75)를 호출할 수 없다.

const 멤버 함수 속성을 사용하면 멤버 함수를 중복정의할 수 있다. 함수를 const 속성으로 중복정의하면 컴파일러는 객체의 const-자격에 가장 근접하게 부합하는 멤버 함수를 선택한다.

다음 예제는 어떻게 (비) const 멤버 함수가 선택되는지 보여준다.
    #include <iostream>
    using namespace std;

    class Members
    {
        public:
            Members();
            void member();
            void member() const;
    };

    Members::Members()
    {}
    void Members::member()
    {
        cout << "비 상수 멤버\n";
    }
    void Members::member() const
    {
        cout << "상수 멤버\n";
    }

    int main()
    {
        Members const constObject;
        Members       nonConstObject;

        constObject.member();
        nonConstObject.member();
    }
    /*
        출력:
        상수 멤버
        비 상수 멤버
    */

설계의 일반 원칙으로서 실제로 객체의 데이터를 변경하지 않는 한, 멤버 함수에 언제나 const 속성을 제공해야 한다.

7.7.1: 익명 객체

객체가 어떤 기능이 있기 때문에 사용되는 경우가 있다. 그 객체의 유일한 존재 이유는 기능 때문이다. 객체 안의 어떤 것도 절대로 바뀌지 않는다. 다음 Print 클래스는 문자열을 인쇄하는 기능을 제공한다. 접두사와 접미사로 구성을 바꿀 수 있다. 부분적으로 클래스 인터페이스를 구현하면:
    class Print
    {
        public:
            Print(ostream &out);
            void print(std::string const &prefix, std::string const &text,
                     std::string const &suffix) const;
    };
이와 같은 인터페이스로 다음과 같은 일을 할 수 있다.
    Print print(cout);
    for (int idx = 0; idx != argc; ++idx)
        print.print("arg: ", argv[idx], "\n");
잘 작동한다. 그러나 print의 변하지 않는 인자를 Print의 생성자에 건네면 크게 개선할 수 있다. 그러면 (인자를 세 개가 아니라 하나만 건네면 되므로) print의 원형이 단순해지고 Print 객체를 기대하는 함수 안에 위의 코드를 싸넣어 인자로 건넬 수 있다.
    void allArgs(Print const &print, int argc, char *argv[])
    {
        for (int idx = 0; idx != argc; ++idx)
            print.print(argv[idx]);
    }
위의 코드는 상당히 범용적인 코드이다. 적어도 Print에 관한 한 그렇다. prefixsuffix는 바뀌지 않으므로 생성자에 건넬 수 있다. 그 원형은 다음과 같다.
    Print(ostream &out, string const &prefix = "", string const &suffix = "");
이제 allArgs는 다음과 같이 사용할 수 있다.
    Print p1(cout, "arg: ", "\n");      // cout에 인쇄
    Print p2(cerr, "err: --", "--\n");  // cerr에 인쇄

    allArgs(p1, argc, argv);            // cout에 인쇄
    allArgs(p2, argc, argv);            // cerr에 인쇄
그러나 이제 p1p2allArgs 함수 안에 사용된다. 게다가 print의 원형을 보면 printPrint 객체가 사용중인 내부 데이터를 변경하지 않는다.

그런 상황이라면 객체를 사용하기 전에 먼저 정의할 필요가 없다. 대신에 익명 객체를 사용할 수 있다. 다음과 같은 경우는 익명 객체를 사용할 수 있다.

함수의 const & 매개변수에 인자로 익명 객체를 건넬 때 그 인자들은 상수로 간주된다. (유형이 클래스인) 객체가 가진 정보를 함수에 건네기 위해서만 존재하기 때문이다. 이런 식으로 수정이 불가능하며 비-const 함수로 사용되지도 않는다. 물론 const_cast를 사용하여 상수 참조의 상수 속성을 걷어낼 수도 있다. 그러나 그것은 나쁜 관행이다. 어떤 변경도 함수가 반환되는 순간에 잃어 버리기 때문이다. 대신에 익명 객체를 받는 함수를 사용하는 편이 좋다. 상수 참조를 초기화하는 데 사용되는 이런 익명 객체를 rvalue 참조와 혼동하면 안된다 (3.3.2항). 존재 목적이 전혀 다르다. rvalue 참조는 주로 함수에 소비되기 위하여 존재한다. 그래서 rvalue 참조로 얻은 정보는 역시 익명인 rvalue 참조 객체보다 더 오래 생존한다.

익명 객체는 생성된 객체에 이름을 제공하지 않고 생성자가 사용될 때 정의된다. 다음은 그에 상응하는 예제이다.

    allArgs(Print(cout, "arg: ", "\n"), argc, argv);    // cout에 인쇄
    allArgs(Print(cerr, "err: --", "--\n"), argc, argv);// cerr에 인쇄
이 상황에서 Print 객체는 생성되는 즉시 첫 인자로 allArgs 함수에 건네진다. 이 함수의 print 매개변수로 접근할 수 있다. allArgs 함수가 실행중인 동안에는 사용할 수 있다. 그러나 일단 함수가 완료되면 익명 Print 객체에는 더 이상 접근할 수 없다.

7.7.1.1: 익명 객체에 따른 미묘함

익명 객체를 사용하면 객체에 대한 const 참조인 함수 매개변수를 초기화할 수 있다. 익명 객체는 그런 함수를 호출하기 바로 전에 생성되고 함수가 끝나면 곧바로 파괴된다. C++의 문법은 다른 상황에서도 익명 객체를 사용할 수 있다. 다음 코드를 연구해 보자:
    int main()
    {
        // 사전 서술문
        Print("hello", "world");
        // 사후 서술문
    }
이 예제에서 익명 Print 객체가 생성된다. 그리고 즉시 파괴된다. 그래서 `사전 서술문' 다음에 Print 객체가 생성된다. 다음 다시 파괴되고 `사후 서술문'이 실행된다.

예제는 표준적인 생명 규칙이 익명 객체에 적용되지 않는다는 것을 보여준다. 익명 객체의 삶은 서술문 안에 국한된다. 자신이 정의되어 있는 블록의 끝에서 소멸하는 것이 아니다.

평범한 익명 객체는 적어도 한 가지 상황에는 유용하다. 프로그램의 실행이 한 지점에 도달하면 어떤 출력을 하도록 코드에 표식을 하고 싶다고 가정해 보자. 객체의 생성자에 표식 기능을 구현할 수 있다. 이름 없는 객체를 정의하여 코드에 표식을 붙일 수 있다.

C++의 문법에 눈에 띄는 특징이 또하나 있다. 다음 예제로 보여준다.

    int main(int argc, char **argv)
    {
        Print p(cout, "", "");              // 1
        allArgs(Print(p), argc, argv);      // 2
    }
이 예제에서 이름있는 p 객체가 서술문 1에서 생성되고 다음 서술문 2에 사용되어 익명 객체를 초기화한다. 익명 객체가 순서대로 사용되어 allArgsconst 참조 매개변수를 초기화한다. 이렇게 기존의 객체를 사용하여 또다른 객체를 초기화하는 방법은 흔한 관례이며, 이른바 복사 생성자의 존재 덕분이다. 복사 생성자는 (생성자이기 때문에) 기존 객체의 특징을 사용하여 객체를 생성하고 그렇게 생성된 객체의 데이터를 초기화한다. 복사 생성자는 제 9장에 깊이 다루겠다. 여기에서는 복사 생성자의 개념만 알아본다.

위의 예제에서 복사 생성자를 사용하여 익명 객체를 초기화한다. 다음으로 그 익명 객체를 사용하여 한 함수의 매개변수를 초기화했다. 그렇지만 같은 기법을 평범한 서술문에 적용하려고 시도하면 (즉, 기존의 객체를 사용하여 익명 객체를 초기화하려고 시도하면) 컴파일러는 에러를 보고한다. p 객체를 정의할 수 없다 (아래 3번 서술문):

    int main(int argc, char **argv)
    {
        Print p("", "");                    // 1
        allArgs(Print(p), argc, argv);      // 2
        Print(p);                           // 3 에러!
    }
이것은 기존의 객체를 사용하여 함수 인자로 사용되는 익명 객체를 초기화하는 것은 괜찮지만 반면에 기존의 객체는 평범한 서술문에서 익명 객체를 초기화하는 데 사용할 수 없다는 뜻인가?

컴파일러는 실제로 이 명백한 모순에 해답을 제공한다. 3번 서술문에 관하여 컴파일러는 다음과 같이 보고한다.

    error: redeclaration of 'Print p'
    에러: 'Print p'를 재선언함
복합 서술문 안에 객체와 변수가 정의되어 있다는 사실을 깨닫을 때 문제가 해결된다. 복합 서술문 안에서 유형 이름 다음에 변수 이름이 오는 것은 변수를 정의하는 문법이다. 반괄호를 사용하면 우선 순위를 깰 수 있다. 그러나 깨야 할 우선 순위가 없다면 아무 효과가 없다. 그냥 컴파일러가 무시할 뿐이다. 서술문 3에서 괄호 덕분에 유형 이름과 변수 이름 사이에 필요한 빈 공백을 제거할 수 있었다. 그러나 컴파일러에게는 이렇게 쓴 셈이다.
        Print (p);
괄호가 과도하기 때문에 다음과 같다.
        Print p;
그리하여 p가 재선언된다.

예를 하나 더 들자면, 내장 유형으로 변수를 정의할 때 괄호가 너무 많으면 컴파일러가 조용히 제거해 버린다.

    double ((((a))));       // 이상하다. 그러나 문제 없다.

익명 변수에 관하여 발견한 사실들을 요약하면:

7.8: `inline' 키워드

Person::name() 함수의 구현을 또 다른 관점에서 살펴보자:
    std::string const &Person::name() const
    {
        return d_name;
    }
이 함수는 Person 클래스 객체의 이름 필드를 보여준다. 예를 들어:
    void showName(Person const &person)
    {
        cout << person.name();
    }
다음과 같이 person의 이름을 삽입한다. 특히 이 조치의 첫 부분은 시간이 좀 걸린다. 왜냐하면 name 필드의 이름을 열람하기 위해 추가로 함수 호출이 필요하기 때문이다. name 함수를 전혀 호출할 필요없이 즉시 d_name 데이터 멤버를 열람하는 편이 더 바람직하다. 이것은 inline 함수로 실현할 수 있다. 인라인 함수는 컴파일러에게 함수를 호출하는 장소마다 함수의 코드를 삽입하라고 요청한다. 이렇게 하면 실행 시간이 빨라진다. 함수를 호출할 필요가 없기 때문인데 함수 호출은 전형적으로 (스택 처리와 매개변수 건네기 등등의) 부담이 약간 따른다. inline은 컴파일러에게 요청하는 것이라는 사실에 주목하라. 컴파일러는 무시하기로 결정할 수도 있다. 함수 몸체에 코드가 너무 많은 경우는 무시할 것이 분명하다. 좋은 프로그래밍 습관을 들이려면 이것을 피하는 것이 좋고 함수의 몸체가 아주 작지 않는 한, inline은 피하는 것이 좋다. 더 자세한 것은 7.8.2항을 참고하자.

7.8.1: 멤버를 인라인으로 정의하기

인라인 함수는 클래스 인터페이스 자체에 구현할 수 있다. 이에 맞게 Person 클래스에 name을 다음과 같이 구현할 수 있다.
    class Person
    {
        public:
            std::string const &name() const
            {
                return d_name;
            }
    };
name 함수의 인라인 코드는 이제 Person 클래스의 인터페이스에 글자 그대로 인라인으로 나타난다. 또 const 키워드를 함수의 헤더에 추가했다.

멤버는 인-클래스로 (즉, 클래스 인터페이스 안에) 정의할 수 있지만 다음과 같은 이유로 나쁜 관례로 간주된다.

위 연구의 결과, 인라인 멤버는 인-클래스로 정의하면 안된다. 그보다 다음과 같이 클래스 인터페이스로 정의해야 한다. 그러므로 Person::name 멤버는 다음과 같이 정의하는 것이 좋다.
    class Person
    {
        public:
            std::string const &name() const;
    };

    inline std::string const &Person::name() const
    {
        return d_name;
    }

혹시라도 Person::name이 인라인 구현을 취소할 필요가 있다면 다음은 그의 비-인라인 구현이다.

    #include "person.ih"

    std::string const &Person::name() const
    {
        return d_name;
    }
inline 키워드만 제거하면 올바른 비-인라인 구현을 얻을 수 있다.

멤버를 인라인으로 정의하면 다음과 같은 효과가 있다. 인라인 정의 함수를 호출할 때마다 컴파일러는 함수의 몸체를 함수 호출 자리에 삽입한다. 함수 자체는 실제로 호출되지 않을 수도 있다.

이런 생성 방법을 인라인 함수라고 부른다. 함수 호출이 아니라 함수 코드 자체가 삽입된다. 인라인 함수를 사용하면 프로그램에 해당 코드가 여러 번 나타날 수 있다는 것을 주의하라. 인라인 함수를 호출할 때마다 사본이 하나씩 나타난다. 함수가 작다면 그리고 빠르게 실행되어야 할 필요가 있다면 아마도 별 문제가 없을 것이다. 그러나 함수의 코드가 크다면 바람직하지 않다. 컴파일러도 이 사실을 인지하고 인라인 함수의 사용을 명령이 아니라 요청으로 처리한다. 함수가 너무 길다고 판단하면 컴파일러는 그 요청을 받아들이지 않는다. 대신에 그냥 보통의 함수처럼 취급한다.

7.8.2: 인라인 함수를 사용해야 할 때

언제 inline 함수를 사용해야 할까? 그리고 언제 사용하면 안 될까? 따라야 할 규칙은 다음과 같다. 모든 인라인 함수는 단점이 하나 있다. 컴파일러가 코드를 실제로 삽입하기 때문에 컴파일 시간에 알려져야 한다. 그러므로 앞서 언급한 바와 같이 인라인 함수는 실행 시간 라이브러리에 위치할 수 없다. 실제로 인라인 함수는 클래스의 인터페이스 근처에서 발견되며 일반적으로 같은 헤더 파일에 존재한다는 뜻이다. 그 결과 헤더 파일은 클래스의 선언 부분 뿐만 아니라 그의 구현 부분도 보여주므로 언제나 인터페이스와 구현 사이의 경계선이 흐려지게 된다.

7.8.2.1: 인라인 함수를 사용하면 안 될때

제 14장 (다형성)에 들어가기 전에 주의할 점이 있다. 인라인 함수를 절대로 피해야 할 경우가 있다. 이 시점에서 전체적으로 정밀하게 다루기에는 약간 이른 감이 있지만 inline 키워드가 이 절의 주제이기 때문에 이곳이 조언을 드릴 적절한 곳이라고 생각한다.

컴파일러가 이른바 애매모호한 링크를 마주하는 상황이 있다.
(http://gcc.gnu.org/onlinedocs/gcc-4.6.0/gcc/Vague-Linkage.html 참고). 이런 상황은 컴파일된 코드를 어떤 객체 파일에 배치할지 컴파일러에게 명확한 지침이 없으면 일어난다. 이런 일은 여러 소스 파일에 흩어져 있는 인라인 함수에 일어난다. 컴파일러는 보통의 인라인 함수 코드를 이런 함수들이 호출되는 곳에 삽입하기 때문에 이런 정상적인 상황에서는 애매모호한 링크는 문제가 되지 않는다.

그렇지만 제 14장에 설명했듯이 다형성을 사용할 때 컴파일러는 inline 키워드를 무시하고 이른바 가상 멤버를 줄 바깥에 선언해야 한다 (out-of-line 함수). 이런 상황에서 애매한 링크는 문제를 일으킬 수 있다. 컴파일된 코드를 어느 객체에 배치할지 컴파일러가 결정해야 하기 때문이다. 일반적으로 함수가 적어도 한 번이라도 호출되면 큰 문제는 아니다. 그러나 가상 함수는 특별하다. 명시적으로 전혀 호출되지 않을 수도 있기 때문이다. (armel 같은) 어떤 골격구조에서는 컴파일러가 그런 인라인 가상 함수를 컴파일하지 못한다. 이 때문에 프로그램에 어떤 심볼은 빠져 버릴 수도 있다. 일이 더 복잡해 진다면 정적 라이브러리가 아니라 공유 라이브러리를 사용할 때 문제가 나타날 수도 있다.

이 모든 문제를 피하려면 가상 함수는 절대 인라인으로 선언하면 안된다. 언제나 줄 밖에 선언하라. 즉, 별도의 소스 파일에 정의해야 한다.

7.8.3: 인라인 변수(Inline variables) (C++17)

인라인 함수 말고도 여러 번역 단위에 (똑같이 초기화되는) 인라인 변수를 정의할 수 있다. 예를 들어 헤더 파일에 다음과 같이 정의할 수 있다.
    inline int value = 15;                      // OK

    class Demo
    {
        // static int s_value = 15;             // ERROR
        static int constexpr s_value = 15;      // OK

        static int s_inline;                    // OK: 아래 참고
                                                //   인라인 정의가 
                                                //   클래스 선언 뒤에 온다.
    };
    inline int Demo::s_inline = 20;             // OK

7.9: 지역 클래스: 함수 안의 클래스

클래스는 전역 수준이나 이름공간 수준에 정의된다. 그렇지만 지역 클래스로 선언하는 것도 완전히 가능하다. 다시 말해, 함수 안에 선언할 수 있다. 그런 클래스를 지역 클래스라고 부른다.

지역 클래스는 상속이나 템플릿이 관련된 고급 어플리케이션에 아주 유용하다 (13.8절). C++ 주해서의 서술 과정상, 이 시점에서는 사용방법을 서술하기가 좀 곤란하지만 주요한 특징은 서술할 수 있다. 이 절의 끝에 예제를 참고하자.

#include <iostream>
#include <string>

using namespace std;

int main(int argc, char **argv)
{
    static size_t staticValue = 0;

    class Local
    {
        int d_argc;             // 비-정적 데이터 멤버 OK

        public:
            enum                // 열거형 OK
            {
                VALUE = 5
            };
            Local(int argc)     // 생성자와 멤버 함수  OK
            :                   // 인-클래스 구현 필수
                d_argc(argc)
            {
                                // 전역 데이터: 접근 가능
                cout << "Local constructor\n";
                                // 정적 함수의 변수들: 접근 가능
                staticValue += 5;
            }
            static void hello() // 정적 멤버 함수들: OK
            {
                cout << "hello world\n";
            }
    };
    Local::hello();             // 지역 정적 멤버 호출
    Local loc(argc);            // 지역 클래스의 실체 정의
}

7.10: `mutable' 키워드

이전 7.7절에 상수 멤버 함수와 상수 객체의 개념을 소개했다.

C++는 변경이 가능한 데이터 멤버의 선언도 허용한다. 심지어 상수 멤버 함수에 의해서도 변경이 가능하다. 그런 데이터 멤버를 클래스 인터페이스에 mutable 키워드로 선언한다.

데이터 멤버를 변경해도 논리적으로 객체를 변경하지 않는다면 mutable을 사용해야 한다. 논리적으로 변경이 없는 것이므로 여전히 상수 객체로 간주할 수 있다.

mutable이 적절하게 사용되는 예는 문자열 클래스의 구현에서 볼 수 있다. std::stringc_str 멤버와 data 멤버를 연구해 보자. 두 멤버가 돌려주는 실제 데이터는 동일하다. 그러나 c_str은 반환된 문자열이 0-바이트로 끝나는 것을 보장한다. 문자열 객체는 길이와 가용능력을 모두 가지고 있으므로 쉽게 c_str을 구현하는 방법은 문자열의 가용능력이 길이를 적어도 1문자만큼 초과하는지 확인하는 것이다. 그래서 c_str을 다음과 같이 구현할 수 있다.

    char const *string::c_str() const
    {
        d_data[d_length] = 0;
        return d_data;
    }
이 구현은 논리적으로 객체의 데이터를 변경하지 않는다. 객체의 최초 문자 갯수(length)를 넘어선 바이트들이 미정의 값을 가지기 때문이다. 그러나 이 구현을 사용하려면 d_datamutable로 선언해야 한다.
    mutable char *d_data;

mutable 키워드는 참조 횟수를 구현한 클래스에도 유용하다. 텍스트에 대하여 참조 횟수를 구현한 클래스를 연구해 보자. 참조 횟수를 세는 객체가 상수 객체일지라도 클래스는 복사 생성자를 정의할 수 있다. 상수 객체는 변경할 수 없기 때문에 어떻게 복사 생성자는 참조 횟수를 증가시킬 수 있을까? 이럴 때 mutable 키워드를 유용하게 사용할 수 있다. 상수 객체일지라도 참조 횟수를 늘리거나 줄일 수 있다.

mutable 키워드는 아껴서 써야 한다. 상수 멤버 함수가 데이터를 변경하더라도 논리적으로 그 객체는 변경되지 않는다. 이를 보여주는 것은 어렵지 않다. 제일 규칙으로서 이 규칙을 어길 아주 확실한 이유가 없다면 mutable을 사용하지 마라 (그 객체는 논리적으로 변경되지 않는다).

7.11: 헤더 파일의 조직

2.5.10항C++ 프로그램이 C 함수를 사용할 때의 헤더 파일을 위한 요구조건을 다루었다. 클래스 인터페이스가 담긴 헤더 파일은 조건이 더 필요하다.

무엇보다도 소스 파일이 필요하다. 소스 파일에 멤버 함수가 코드로 담긴다. 여기에 기본적으로 두 가지 접근법이 있다.

첫 번째 방법은 컴파일러에 경제적이라는 장점이 있다. 특정한 소스 파일에 필요한 헤더 파일만 읽으면 된다. 단점은 개발자가 손수 소스 파일에 여러 헤더 파일을 포함하고 또 포함해야 한다는 것이다. include 지시어를 타자하는 시간도 걸리고 특정한 소스 파일에 필요한 헤더 파일에 관하여 생각하는 것도 시간이 걸린다.

두 번째 방법은 프로그램 개발자에게 경제적이다. 클래스의 헤더 파일은 헤더 파일을 축적한다. 그래서 일반적으로 점점 더 유용하게 된다. 단점은 컴파일러가 불필요하게 많은 헤더 파일을 처리해야 한다는 것이다. 실제로는 컴파일할 함수가 사용되지 않더라도 말이다.

컴퓨터는 날이 갈수록 점점 더 빨라지기 때문에 (그리고 컴파일러는 나날이 똑똑해지기 때문에) 필자는 두 번째 방법이 첫 번째 방법보다 더 좋다고 생각한다. 그래서 그 출발점으로서 다음 예제에 맞게 특별한 MyClass 클래스의 소스 파일을 조직할 수 있다.

    #include <myclass.h>

    int MyClass::aMemberFunction()
    {}
include 지시어 하나만 있다. 이 지시어는 INCLUDE 파일 환경 변수에 언급된 디렉토리에 있는 헤더 파일을 참조한다는 것을 눈여겨보라. 지역 헤더 파일(#include "myclass.h")도 사용할 수 있지만 그러면 클래스 헤더 파일 자체의 조직이 약간 복잡해지는 경향이 있다.

헤더 파일 자체의 조직은 주의가 좀 필요하다. 다음 예제를 연구해 보자. File 클래스와 String 클래스를 사용한다.

File 클래스에 gets(String &destination) 멤버가 있는 반면에 String 클래스는 getLine(File &file) 멤버 함수가 있다고 간주하자. 그러면 class String에 대한 (부분적인) 헤더 파일은 다음과 같다.

    #ifndef STRING_H_
    #define STRING_H_

    #include <project/file.h>   // File에 관하여 알기 위하여

    class String
    {
        public:
            void getLine(File &file);
    };
    #endif
File 클래스도 비슷하게 설정해야 한다.
    #ifndef FILE_H_
    #define FILE_H_

    #include <project/string.h>   // String에 관하여 알기 위하여

    class File
    {
        public:
            void gets(String &string);
    };
    #endif
불행하게도 이제 문제가 발생한다. 컴파일러는 File::gets 함수의 소스 파일을 다음과 같이 컴파일하려고 시도한다. 이 문제에 대한 해결책은 클래스 인터페이스보다 먼저 전방 클래스 참조를 사용하고 그리고 클래스 인터페이스를 지나서 상응하는 클래스 헤더 파일을 포함하는 것이다. 그래서 다음과 같이 된다.
    #ifndef STRING_H_
    #define STRING_H_

    class File;                 // 전방 참조

    class String
    {
        public:
            void getLine(File &file);
    };

    #include <project/file.h>   // File에 관하여 알기 위하여

    #endif
File 클래스도 비슷하게 설정해야 한다.
    #ifndef FILE_H_
    #define FILE_H_

    class String;               // 전방 참조

    class File
    {
        public:
            void gets(String &string);
    };

    #include <project/string.h>   // String에 관하여 알기 위하여

    #endif
이것은 다른 클래스를 가리키는 참조 또는 포인터가 관련되어 있는 모든 상황에 잘 작동한다. 그리고 클래스 유형의 반환 값이나 매개변수를 가진 (비-인라인) 멤버 함수에 잘 작동한다.

이 설정은 합성과 작동하지 않으며 인클래스 멤버함수와 인라인 멤버 함수와도 작동하지 않는다. 합성된 String 클래스의 데이터 멤버가 File 클래스에 있다고 가정해 보자. 이 경우, File 클래스의 인터페이스는 자신보다 먼저 String 클래스의 헤더 파일을 포함해야 한다. 그렇지 않으면 컴파일러가 File 객체의 크기를 알 수 없기 때문이다. File 객체는 String 멤버를 담고 있지만 컴파일러는 String 데이터 멤버의 크기를 알 수 없다. 그 때문에 File 객체의 크기를 결정하지 못한다.

클래스에 합성 객체가 담겨 있으면 (또는 다른 클래스로부터 파생되면, 제 13장 참고) 합성 객체의 클래스의 헤더 파일들은 자신의 클래스 인터페이스보다 먼저 컴파일러에게 읽혀야 한다. 그런 경우 class File은 다음과 같이 정의할 수 있다.

    #ifndef FILE_H_
    #define FILE_H_

    #include <project/string.h>     // String에 관하여 알기 위하여

    class File
    {
        String d_line;              // 합성 !

        public:
            void gets(String &string);
    };
    #endif
String 클래스는 File 객체를 합성 객체로 선언할 수 없다. 그렇게 되면 이 클래스의 소스를 컴파일하는 동안 또다시 미정의 클래스가 생기기 때문이다.

(클래스 인터페이스 아래에 나타나는) 나머지 헤더 파일은 모두 클래스의 소스 파일에 사용되기 때문에 필요하다.

이 접근법을 사용하면 더 세밀하게 다듬을 수 있다.

7.11.1: 헤더 파일에 이름공간 사용하기

이름공간에 있는 객체를 일반 헤더 파일에 사용할 때 using 지시어를 지정하면 안된다. 일반 헤더 파일은 라이브러리로부터 다른 개체나 클래스를 선언하므로 using 지시어가 사용되면 그 헤더 파일이 포함된 모든 코드에 그 선언을 받아들이고 사용하지 않을 수 없다.

special이라는 이름공간에 Inserter cout이라는 객체를 선언하더라도 special::coutstd::cout과 다른 객체이다. Flaw 클래스를 생성할 때 이 클래스의 생성자가 special::Inserter를 가리키는 참조를 기대한다면 그 클래스는 다음과 같이 생성해야 한다.

    class special::Inserter;

    class Flaw
    {
        public:
            Flaw(special::Inserter &ins);
    };
이제 Flaw 클래스를 설계하는 일이 재미가 없어진다. 끊임없이 개체마다 special::을 붙이는 것이 지겨울 것이다. 그래서 다음의 생성 방법이 사용된다.
    using namespace special;

    class Inserter;
    class Flaw
    {
        public:
            Flaw(Inserter &ins);
    };
이것은 잘 작동한다. 누군가 다른 소스 파일에 flaw.h를 포함하고 싶어하는 순간까지는 말이다. using 지시어 때문에 이제 special 이름공간도 사용하고 있다. 이 때문에 예상치 못한 부작용이 생길 수 있기 때문이다.
    #include <flaw.h>
    #include <iostream>

    using std::cout;

    int main()
    {
        cout << "starting\n";       // 컴파일 불가
    }
컴파일러는 cout에 대하여 두 가지 해석에 마주한다. 먼저, flaw.h 헤더 파일에 있는 using 지시어 때문에 컴파일러는 coutspecial::Inserter으로 생각하게 된다. 다음, 사용자 프로그램에 있는 using 지시어 때문에 coutstd::ostream이라고 간주하게 된다. 결과적으로 컴파일러는 에러를 보고한다.

제일 규칙으로서 일반적 목적의 헤더 파일은 using 선언을 포함하면 안된다. 이 규칙은 클래스의 소스에만 포함된 헤더 파일에는 해당하지 않는다. 여기에는 자유롭게 얼마든지 많이 using 선언을 적용해도 된다. 이 지시어들은 다른 소스에 절대로 도달하지 않기 때문이다.

7.12: 클래스 데이터 멤버에 적용된 sizeof

C++에서 유명한 sizeof 연산자는 실체를 지정할 필요없이 클래스의 데이터 멤버에 적용할 수 있다. 다음을 연구해 보자:
    class Data
    {
        std::string d_name;
        ...
    };
Datad_name 멤버의 크기를 얻으려면 다음 표현식을 사용할 수 있다.
    sizeof(Data::d_name);
그렇지만 컴파일러가 데이터 보호를 준수하고 있다는 사실도 눈여겨보라. sizeof(Data::d_name)d_name 멤버가 보일 경우에만 사용할 수 있다. 즉, Data의 멤버 함수와 친구 함수만 사용할 수 있다.