[c++] 멤버 초기화 목록을 선호하는 이유는 무엇입니까?

생성자와 함께 멤버 초기화 목록을 사용하는 것에 부분적이지만 … 이후의 이유를 잊어 버린 지 오래되었습니다 …

생성자에서 멤버 초기화 목록을 사용합니까? 그렇다면 왜 그렇습니까? 그렇지 않다면 왜 안됩니까?



답변

들어 POD의 클래스 멤버, 그것은 스타일의 단지 문제, 차이가 없습니다. 클래스 인 클래스 멤버의 경우 기본 생성자를 불필요하게 호출하지 않습니다. 치다:

class A
{
public:
    A() { x = 0; }
    A(int x_) { x = x_; }
    int x;
};

class B
{
public:
    B()
    {
        a.x = 3;
    }
private:
    A a;
};

이 경우에 대한 생성자 B는에 대한 기본 생성자를 호출 A한 다음 a.x3으로 초기화 합니다. 더 좋은 방법은 의 생성자가 초기화 목록에서 B의 생성자를 직접 호출 A하는 것입니다.

B()
  : a(3)
{
}

이것은 기본 생성자가 아닌 AA(int)생성자를 호출 합니다. 이 예제에서 그 차이는 무시할 만하지 만 A메모리를 할당하거나 파일을 여는 등의 기본 생성자가 더 많은 작업 을 수행한다고 가정 해보십시오 . 불필요하게 그렇게하고 싶지 않을 것입니다.

클래스는 기본 생성자가없는, 또는 당신이있는 경우 또한, const멤버 변수를, 당신은 해야한다 이니셜 라이저 목록을 사용하여 :

class A
{
public:
    A(int x_) { x = x_; }
    int x;
};

class B
{
public:
    B() : a(3), y(2)  // 'a' and 'y' MUST be initialized in an initializer list;
    {                 // it is an error not to do so
    }
private:
    A a;
    const int y;
};


답변

위에서 언급 한 성능상의 이유 외에도 클래스가 생성자 매개 변수로 전달 된 객체에 대한 참조를 저장하거나 클래스에 const 변수가있는 경우 초기화 목록을 사용하는 것 외에는 선택할 수 없습니다.


답변

  1. 기본 클래스의 초기화

답변에 언급되지 않은 생성자 이니셜 라이저 목록을 사용하는 중요한 이유 중 하나는 기본 클래스의 초기화입니다.

건설 순서에 따라 기본 클래스는 하위 클래스보다 먼저 구성되어야합니다. 생성자 이니셜 라이저 목록이 없으면 기본 클래스에 기본 생성자가 있으면 자식 클래스의 생성자를 입력하기 직전에 호출됩니다.

그러나 기본 클래스에 매개 변수화 된 생성자 만있는 경우 생성자 이니셜 라이저 목록을 사용하여 기본 클래스가 자식 클래스보다 먼저 초기화되도록해야합니다.

  1. 매개 변수화 된 생성자 만있는 서브 오브젝트의 초기화

  2. 능률

생성자 이니셜 라이저 목록을 사용하면 먼저 데이터 멤버를 기본 상태로 초기화 한 다음 상태를 코드에서 필요한 상태로 변경하지 않고 코드에서 필요한 상태로 데이터 멤버를 초기화합니다.

  1. 비 정적 const 데이터 멤버 초기화

클래스의 정적이 아닌 const 데이터 멤버에 기본 생성자가 있고 생성자 이니셜 라이저 목록을 사용하지 않으면 기본 상태로 초기화되므로 의도 한 상태로 초기화 할 수 없습니다.

  1. 참조 데이터 멤버의 초기화

참조가 나중에 선언되고 초기화 될 수 없으므로 컴파일러가 생성자를 입력 할 때 참조 데이터 멤버를 초기화해야합니다. 이것은 생성자 이니셜 라이저 목록에서만 가능합니다.


답변

성능 문제 외에도 코드 유지 관리 및 확장 성이라고 부르는 또 다른 중요한 문제가 있습니다.

T가 POD이고 초기화 목록을 선호하기 시작하면 한 번 T가 비 POD 유형으로 변경되면 불필요한 최적화 생성자 호출이 이미 최적화되어 있기 때문에 초기화 주위에서 아무것도 변경하지 않아도됩니다.

유형 T에 기본 생성자와 하나 이상의 사용자 정의 생성자가 있고 기본 생성자를 제거하거나 숨기려고 한 경우 초기화 목록이 사용 된 경우 사용자 정의 생성자가 코드를 업데이트 할 필요가 없습니다. 그들은 이미 올바르게 구현되었습니다.

const 멤버 또는 참조 멤버와 동일하게 처음에 T가 다음과 같이 정의되었다고 가정 해 봅시다.

struct T
{
    T() { a = 5; }
private:
    int a;
};

다음으로, 처음부터 초기화 목록을 사용하는 경우 const로 한정하기로 결정한 경우 한 줄 변경이지만 위와 같이 T를 정의한 경우 할당자를 제거하기 위해 생성자 정의를 파헤쳐 야합니다.

struct T
{
    T() : a(5) {} // 2. that requires changes here too
private:
    const int a; // 1. one line change
};

코드를 “코드 원숭이”가 아니라 자신이하고있는 일에 대해 더 깊이 고려하여 결정을 내리는 엔지니어가 코드를 작성하면 유지 관리가 훨씬 쉽고 오류가 덜 발생한다는 사실은 비밀이 아닙니다.


답변

생성자의 본문이 실행되기 전에 상위 클래스 및 필드의 모든 생성자가 호출됩니다. 기본적으로 인수가없는 생성자가 호출됩니다. 초기화 목록을 사용하면 호출 할 생성자와 생성자가받을 인수를 선택할 수 있습니다.

참조 또는 const 필드가 있거나 사용 된 클래스 중 하나에 기본 생성자가없는 경우 초기화 목록을 사용해야합니다.


답변

// Without Initializer List
class MyClass {
    Type variable;
public:
    MyClass(Type a) {  // Assume that Type is an already
                     // declared class and it has appropriate 
                     // constructors and operators
        variable = a;
    }
};

여기서 컴파일러는 다음 단계에 따라 MyClass
1 유형의 객체를 만듭니다 . “a”에 대해 유형의 생성자가 먼저 호출됩니다.
2.“Type”의 할당 연산자는 MyClass () 생성자의 본문 내에서 호출됩니다.

variable = a;
  1. 그리고 마지막으로“Type”의 소멸자는 범위를 벗어나기 때문에“a”가 필요합니다.

    이제 Initializer List가있는 MyClass () 생성자와 동일한 코드를 고려하십시오.

    // With Initializer List
     class MyClass {
    Type variable;
    public:
    MyClass(Type a):variable(a) {   // Assume that Type is an already
                     // declared class and it has appropriate
                     // constructors and operators
    }
    };

    이니셜 라이저 목록을 사용하면 다음 단계에 따라 컴파일러가 이어집니다.

    1. “Type”클래스의 복사 생성자가 다음과 같이 초기화됩니다. variable (a). 이니셜 라이저 목록의 인수는 구문 “변수”를 직접 복사하는 데 사용됩니다.
    2. “Type”의 소멸자는 범위를 벗어나므로“a”가 필요합니다.

답변

추가 정보를 추가하여 멤버 초기화 목록이 얼마나 차이가 나는지 보여줄 수 있습니다 . leetcode 303 범위 합계 쿼리-불변, https://leetcode.com/problems/range-sum-query-immutable/ 에서 특정 크기의 벡터를 0으로 구성하고 초기화해야합니다. 다음은 두 가지 다른 구현 및 속도 비교입니다.

멤버 초기화 목록이 없으면 AC를 얻으려면 약 212ms가 소요 됩니다.

class NumArray {
public:
vector<int> preSum;
NumArray(vector<int> nums) {
    preSum = vector<int>(nums.size()+1, 0);
    int ps = 0;
    for (int i = 0; i < nums.size(); i++)
    {
        ps += nums[i];
        preSum[i+1] = ps;
    }
}

int sumRange(int i, int j) {
    return preSum[j+1] - preSum[i];
}
};

이제 멤버 초기화 목록을 사용하여 AC를 얻는 데 걸리는 시간은 약 108ms 입니다. 이 간단한 예제를 통해 멤버 초기화 목록이 더 효율적임을 알 수 있습니다. . 모든 측정은 LC의 실행 시간에서 이루어집니다.

class NumArray {
public:
vector<int> preSum;
NumArray(vector<int> nums) : preSum(nums.size()+1, 0) {
    int ps = 0;
    for (int i = 0; i < nums.size(); i++)
    {
        ps += nums[i];
        preSum[i+1] = ps;
    }
}

int sumRange(int i, int j) {
    return preSum[j+1] - preSum[i];
}
};