[C++] 클래스의 초기화 리스트 (Initializer List)를 사용하는 이유

초기화 리스트 (Initializer List)를 사용하는 이유

초기화 리스트는 클래스의 생성자 뒤에 덧붙인 멤버들의 초기화 목록입니다.

생성자 뒤에 콜론을 사용해서 이 뒤로 초기화 리스트가 이어진다라는 것을 알려줍니다.

 

아래의 예제에서 초기화 리스트를 보여주고 있습니다.

class CInit {
    int m_nValue;
    string m_str;

public:
    CInit() : m_nValue(10), m_str("String"){}	// 초기화 리스트
};

 

그런데, 초기화 리스트를 통해 초기화한 것과 생성자 안에서 초기화를 한 것은 같은 결과를 가져옵니다.

CInit::CInit(){
    m_nValue = 10;
    m_str = "String";
}

왜 초기화 방식이 여러 개가 되도록 만들어 둔 걸까요?

 

그것은 생성자 안에서 초기화를 할 수가 없는 상황이 있기 때문입니다.

그리고 성능 향상에 관한 측면도 있습니다.

 

상수 멤버 초기화

상수 객체는 선언과 동시에 초기화해야 하는 것을 알고 있을 겁니다.

클래스의 상수 멤버라고 생성자 함수가 호출될 때까지 초기화를 하지 않아도 괜찮은 것은 아닙니다.

 

아래의 예제와 같이 생성자 안에서 초기화를 하게 되면 오류가 발생합니다.

class CInit {
    const int m_nValue;
    
public:
    CInit();
};

CInit::CInit(){
    m_nValue = 10;  // compile error !!
}

 

이러한 상수 멤버를 초기화하는 방법은 두 가지가 있습니다. 

class CInit {
    const int m_nValue = 10;    // 선언과 동시에 초기화
    int m_nNonConstValue = 20;
public:
    CInit();  
};

첫 번째는 위와 같이 선언과 동시에 초기화하는 방법입니다.

 

C++11 버전 이후로, 이 방식은 상수 멤버뿐만 아니라, 모든 멤버에 대하여 선언과 함께 초기화가 가능하게 되었습니다. 

이 기능을 "non-static data member initialization"라고 합니다.

 

non-static 멤버의 초기화라고 해서 정적 멤버로 한정하는 이유는 뭘까요?

정적 멤버는 이전부터 선언과 동시에 초기화가 가능했기 때문입니다.

 

두 번째는 이 글에서 말하고 있는 초기화 리스트를 작성해서 초기화하는 방법입니다.

class CInit {
    const int m_nValue;    
    
public:
    CInit() : m_nValue(11) {}
};

 

참고로, 클래스 멤버 선언 시 초기화하는 것과, 초기화 리스트에서 초기화하는 것을 동시에 하면 어떻게 될까요?

class CInit {
    const int m_nValue = 10;    // 선언 시 초기하
    
public:
    CInit() : m_nValue(11) {}	// 초기화 리스트에서 초기화
};

컴파일러는 선언 시 초기화하는 것을 초기화 리스트에서 초기화하는 것처럼 처리합니다.

그런데, 초기화 리스트에서 명시적으로 멤버를 초기화했으면, 선언 시 초기화한 것은 무시됩니다.

이것은 컴파일 오류가 발생하지 않습니다. 

 

따라서, 위 예제의 경우 m_nValue의 값은 11입니다.

 

레퍼런스(reference) 멤버 초기화

레퍼런스 멤버도 선언 시 초기화가 되어야 합니다.

생성자 안에서 초기화하면 상수 멤버와 마찬가지로 오류를 만나게 됩니다.

class CInit {
    int& m_nRef;    
    
public:
    CInit(int& ref);
};

CInit::CInit(int& ref){
    m_nRef = ref;  // compile error !!
}

 

따라서, 매개 변수 생성자의 초기화 리스트에서 초기화를 해야 됩니다.

class CInit {
    int& m_nRef;    
    
public:
    CInit(int& ref) : m_nRef(ref){}
};

 

만약, 위의 클래스에서 생성자를 작성하지 않으면 어떻게 될까요?

class CInit {
    int& m_nRef;    
};

클래스를 작성할 때는, 컴파일해도 문제가 생기지 않습니다.

그러나, 클래스를 인스턴스화하는 순간 m_nRef 레퍼런스 멤버가 초기화되지 않았음을 알려주는 오류를 만나게 됩니다.

class CInit{
    int& m_nRef;
};

int main(){

    CInit cInstance;	// compile error !!
    
    return 0;
}

CInit 클래스에 생성자를 하나도 작성하지 않았으므로, 기본(Default) 생성자가 자동으로 생성되고 호출되기 때문입니다.

그럼, 기본 생성자를 작성하고, 초기화 리스트에서 m_nRef를 초기화하면 되겠지라고 하지만 참조할 대상을 지정할 수 없습니다.

 

따라서, 매개 변수 생성자를 작성하고, 초기화 리스트에서 m_nRef를 초기해야 합니다.

 

기본 생성자가 없는 객체 멤버의 초기화

기본 생성자 없이 매개 변수 생성자만 있는 객체 멤버도 초리화 리스트에서 초기화를 해야 됩니다.

class CA {
    int m_Value;

public:
    CA( int val) : m_Value(val) {}
};

class CB {
    CA ca;	// 객체 멤버
    
public:
    CB(){}	// compile error !!
};

 

오류를 해결하려면, CA 클래스에 기본 생성자를 만들거나, CB에서 초기화 리스트를 작성하면 됩니다.

class CA {
    int m_Value;

public:
    CA( int val) : m_Value(val) {}
};

class CB {
    CA ca;
public:
    CB() : ca(10) {}	// 초기화 리스트에서 초기화
};

 

기본(base) 클래스의 초기화

기본 클래스의 매개변수 생성자를 호출하려면 초기화 리스트를 사용해야 합니다.

class CA {
    int m_Value;

public:
    CA( int val) : m_Value(val) {
        cout << "CA constructor called\n";
    }
};

// 클래스 CB는 CA에서 상속됨
class CB : CA {

public:
    CB(int val) : CA(val) {		// CA의 매개 변수 생성자 호출
    
        cout << "CB constructor called\n";
    }
};

 

성능 향상

초기화 리스트를 사용하면, 생성자 안에서 초기화하는 경우에 비해 비용이 적게 듭니다.

따라서, 성능 향상이 되는 효과가 있습니다.

 

우선 비교를 위해 사용하게 될 클래스 CA를 정의합니다.

초기화 코드를 중복시키지 않기 위해서, SetupInit 함수를 따로 작성했습니다.

class CA {
    void SetupInit(const CA& other){
        // Do somethings
    }

public:

    CA(){
        cout << "CA base constructor called\n";
        // Do default initialization
    }

    CA( const CA& other){
        cout << "CA parameterized constructor called\n";
        SetupInit(other);
    }
    
    void operator= (const CA& other){
        cout << "CA = operator called\n";
        SetupInit(other);
    }
};

 

그리고, 클래스 CA를 멤버로 가진 클래스 CB를 초기화할 때 과정을 생각해 보죠.

 

우선, 생성자 안에서 초기화할 때는 클래스 CA의 기본 생성자가 호출됩니다.

그 후에 대입 연산자가 호출되죠.

class CB{
    CA ca;

public:
    CB(const CA& other){
        ca = other;
    }
};

 

그렇지만, 초기화 리스트를 사용하는 경우에는 바로 매개 변수 생성자가 호출됩니다.

class CB{
    CA ca;

public:
    CB(const CA& other) : ca(other) {}
};

 

정리

  • 클래스 생성자 내에서 초기화할 수 없는 상황이 있어서 초기화 리스트를 사용한다.
  • 초기화 리스트를 사용하는 경우, 성능이 향상된다.

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유