4 분 소요

멤버 초기화 리스트

객체 초기화의 임무를 띤 생성자가 하는 주된 일은 멤버 변수의 값을 초기화하는 것이다. 그래서 생성자의 본체는 보통 전달받은 인수를 멤버 변수에 대입하는 대입문으로 구성된다. 멤버에 단순히 값을 대입하기만 하는 경우 본체에서 = 연산자를 쓰는 대신 초기화 리스트(Member Initialization List)라는 것을 사용할 수 있다. 초기화 리스트는 함수 선두와 본체 사이에 :을 찍고 멤버와 초기값의 대응 관계를 나열하는 것이다. Position 생성자를 초기화 리스트로 작성하면 다음과 같다.

Position(int ax, int ay, char ach) : x(ax),y(ay),ch(ach)
{
     // 더 하고 싶은 일
}

초기화 리스트의 항목은 “멤버(인수)”의 형태를 띠며 멤버=인수 대입 동작을 한다. 단순한 대입만 가능하며 복잡한 계산을 한다거나 함수를 호출하는 것은 불가능하다. 위의 Position 생성자는 초기화 리스트의 지시대로 x는 ax로, y는 ay로, ch는 ach로 초기화한다. 초기화 리스트에서 모든 멤버에 값을 대입했으므로 본체는 아무 것도 할 일이 없어졌는데 물론 더 필요한 초기화가 있다면 본체에 추가 코드를 작성할 수 있다.

생성자 본체에서 값을 직접 대입하는 것과 초기화 리스트의 효과는 동일하므로 둘 중 편한 방법을 사용하면 된다. 그러나 다음 몇 가지 경우에는 본체에 대입문을 쓸 수 없으므로 반드시 초기화 리스트로 멤버를 초기화해야 한다. 주로 대입 연산을 쓸 수 없는 특수한 멤버의 경우이다.

상수 멤버 초기화

상수는 선언할 때 반드시 초기화해야 한다. const int year = 365; 의 형식으로 상수를 선언하는데 여기서 = 365를 빼 버리면 다시는 이 상수값을 정의할 수 없으므로 에러로 처리된다. 단, 클래스의 멤버일 때는 객체가 만들어질 때까지 초기화를 연기할 수 있으며 생성자의 초기화 리스트에서만 초기화 가능하다.

class Some
{
public:
    const int value;
    Some(int i) : value(i) { }
    void OutValue() { printf("%d\n", value); }

};

void main()
{
    Some s(5);
    s.OutValue();
}

Some 클래스는 정수형의 상수 Value를 멤버로 가지고 있는데 상수에 대해서는 대입 연산자를 사용할 수 없다. 상수의 정의에 의해 다음 코드는 당연히 불법이다.

Some(int i) { value = i; }

Value 멤버는 상수이므로 값을 변경할 수 없으며 대입 연산 자체가 인정되지 않는다. 그래서 초기화 리스트라는 특별한 문법이 필요하다. 초기화 리스트는 본체 이전의 특별한 영역이며 생성자에서만 이 문법이 적용된다. 상수는 원래 선언할 때 초기값을 주어야 하나 클래스 정의문에 다음과 같이 초기값을 주는 것은 불가능하다.

class Some
{
public:
    const int value = 5;
}

클래스 선언문은 컴파일러에게 클래스가 어떤 모양을 하고 있다는 것을 알릴 뿐이지 실제 메모리를 할당하지는 않는다. 그러므로 Value 멤버는 아직 메모리에 실존하지 않으며 존재하지도 않는 대상의 값을 초기화할 수는 없다. 상수는 객체가 생성될 때 반드시 초기화되어야 하며 상수 멤버 초기화의 책임은 생성자에게 있다. 따라서 상수 멤버를 가지는 클래스의 모든 생성자들은 상수 멤버에 대한 초기화 리스트를 가져야 한다. 만약 이를 위반할 경우 에러로 처리된다.

이외에 상수 멤버값을 정적으로 선언하는 방법과 열거 멤버를 상수 대신 사용하는 방법이 있는데 다음에 상세히 알아보도록 하자. 여기서는 상수 멤버의 초기값을 주기 위해 초기화 리스트를 사용한다는 것만 알아 두자

레퍼런스 멤버 초기화

레퍼런스는 변수에 대한 별명이며 선언할 때 반드시 누구에 대한 별명인지를 밝혀야 한다. 단, 예외적으로 함수의 형식 인수, 클래스의 멤버, extern 선언시는 대상체를 지정하지 않을 수 있는데 이때는 함수 호출시나 객체 생성시로 초기화가 연기된다. 레퍼런스 멤버를 가지는 클래스는 생성자에서 이 멤버를 초기화해야 하는데 다음 예제처럼 초기화 리스트를 사용한다.

class Some
{
public:
    int &ri;
    Some(int &i) : ri(i) { }
    void OutValue() { printf("%d\n", ri); }
};

void main()
{
     int i = 5;
     Some s(i);
     s.OutValue();
}

Some 클래스는 정수형 레퍼런스 변수 ri를 멤버로 가지고 있으며 생성자는 ri가 참조할 실제 변수를 인수로 전달받아 ri가 이 변수의 별명이 되도록 한다. 레퍼런스 멤버는 다음과 같이 대입 연산자로 초기화할 수 없다.

Some(int &i) { ri = i; }

왜냐하면 레퍼런스에 대한 대입 연산은 레퍼런스 그 자체의 대상체를 지정하는 것이 아니라 레퍼런스가 참조하고 있는 변수에 값을 대입하는 것으로 정의되어 있기 때문이다. 레퍼런스 멤버는 대입 연산자로 초기화할 수 없으며 반드시 초기화 리스트에서 대상체를 지정해야 한다. 레퍼런스 멤버 초기화 문법은 사실 앞의 상수 멤버 초기화 규칙과 동일한 것이라고 볼 수 있다. 왜냐하면 레퍼런스는 일종의 상수 포인터이기 때문이다.

레퍼런스는 생성 직후부터 별명으로 동작해야 하므로 선언할 때 짝이될 변수를 반드시 지정해야 한다. 그러나 레퍼런스를 초기식없이 선언할 수 있는 세 가지 예외적인 경우가 있는데 그 중 한가지가 바로 클래스의 멤버로 선언될 때이다. 이 경우 생성자는 레퍼런스의 짝을 찾아 주어야 할 막중한 임무를 띠며 만약 이 임무를 소홀히 할 경우 컴파일러로부터 섭섭하다는 에러 메시지를 받게 된다. 짝이 없는 레퍼런스는 절대로 존재할 수 없다.

포함된 객체 초기화

구조체끼리 중첩할 수 있듯이 클래스도 다른 클래스의 객체를 멤버로 가질 수 있다. 포함된 객체를 초기화할 때도 초기화 리스트를 사용한다.

class Position
{
public:
    int x, y;
    Position(int ax, int ay) 
    { 
        x = ax;
        y = ay; 
    }
};

class Some
{
public:
    Position pos;
    Some(int x, int y) : pos(x, y) { }
    void OutValue() { printf("%d, %d\n", pos.x, pos.y); }
};

void main()
{
    Some s(3,4);
    s.OutValue();
}

Some 클래스가 Position 클래스의 객체 Pos를 포함하고 있는데 포함된 Pos 객체를 초기화하기 위해 생성자를 다음과 같이 작성할 수는 없다.

Some(int x, int y) { pos(x, y); }

왜냐하면 생성자는 객체를 생성할 때만 호출할 수 있으며 외부에서 명시적으로 호출할 수 없기 때문이다. 그래서 멤버로 포함된 객체를 초기화할 때도 초기화 리스트를 사용해야 한다. 그렇다면 다음 코드는 어떨까?

Some(int x, int y) { Position pos(x, y); }

생성자를 호출하는 문장처럼 보이지만 pos는 생성자 함수내에서 임시적으로 만들어지는 지역 객체일 뿐이며 포함된 객체 pos와는 이름만 같을 뿐 아무런 상관이 없다. 이 코드는 포함 객체 Pos를 초기화하는 것이 아니라 쓰지도 않는 지역 객체를 멤버와 같은 이름으로 하나 만들 뿐이며 이 객체는 생성자가 종료될 때 자동으로 파괴된다. 기본 타입의 멤버 변수도 일종의 포함된 객체로 볼 수 있으므로 x(ax), y(ay)식으로 초기화 리스트에서 초기화할 수 있다. 물론 기본 타입은 대입 연산에 의해 값을 대입할 수도 있으므로 생성자 본체에서 초기화하는 것도 가능하다.

만약 포함된 객체가 디폴트 생성자를 정의한다면 초기화 리스트에서 초기화하지 않아도 컴파일러가 디폴트 생성자를 호출하며 에러는 발생하지 않는다. 그러나 디폴트 생성자는 쓰레기를 치우는 정도 밖에 할 수 없으므로 원하는 초기화는 아닐 확률이 높다. 그렇지 않은 경우에는 반드시 초기화 리스트에서 적절한 생성자를 호출하여 포함된 객체를 초기화해야 한다.