I'm FanJae.

[C++] Member initializer list, Default member initializer 본문

C++/Basic

[C++] Member initializer list, Default member initializer

FanJae 2024. 8. 14. 12:04

※ 본 포스트는 코드누리 C++ Basic 강의 내용을 보고 정리한 포스트입니다.

 

1. 멤버 데이터를 초기화 하는 3가지 방법

- 멤버 데이터를 초기화 하는 방법은 크게 3가지가 존재한다.

 

① 지금까지 사용한 방법

class Point
{
   int x;
   int y;
public:
   Point(int a, int b)
   {
       x = a;
       y = b;
   }
};

- 우리가 알고 있는 가장 일반적인 방법이다.

 

② Member initializer list (C++98)

class Point
{
   int x;
   int y;
public:
   Point(int a, int b) : x{a}, y{b}
   {
   }
};

 

③ Default member initializer (C++11)

class Point
{
   int x{0};
   int y{0};
};

- 오늘은 Case 2번과 Case 3번에 대해 알아보고자 한다.


2. Member initializer list

#include <string>

class People1
{
    std::string name;
    int age;
public:
    People1(const std::string &s, int a)
    {
        name = s;
        age = a;
    }
};

class People2
{
    std::string name;
    int age;
public:
    People2(const std::string &s, int a) : name{s}, age{a}
    {
    }
};

- People1과 People2는 초기화 방법만 제외하면 동일해 보인다.

- 하지만 이 둘은 '대입'과 '초기화'라는 차이가 있다.

People1(const std::string &s, int a)
{
     name = s; // 대입
     age = a;
}

People2(const std::string &s, int a) : name{s}, age{a} // 초기화
{
}

- 한쪽은 대입, 한쪽은 초기화를 의미한다.

- 초기화(initialization)이란, 객체를 생성하면서 초기값을 전달하는 것이다.

- 이에 반면, 대입은 객체를 생성한 후, 값을 전달한다. 

std::string name;
name = s; // 대입 연산자가 호출 된다.

- 즉, 대입의 경우 name 객체를 생성하는 과정에서 디폴트 생성자가 한번 호출되고, 대입 연산자가 호출된다는 것이다.

- 대입 연산자에 대해서는 추후 포스트에서 더 정리해보겠다.

 

2-1. People1과 People2 객체 생성의 차이

People1 p1(s, 20); // People1 객체 생성

std::string name;
int age;
name = s; // string 내부에 구현된 대입 연산자가 호출 됨.
age = a;

- 즉, string의 대입 연산자의 호출이 일어나게 된다.

People2 p2(s, 20); // People2 객체 생성

std::string name{s}; // 내부에 구현된 인자가 한 개인 생성자가 호출
int age = a;

- 반면 People2는 내부에 구현된 인자가 한 개인 생성자가 호출되어, 대입 연산자 호출 과정이 일어나지 않는다.

 

2-2. std::string 클래스의 정체

- 원래 std::string은 "basic_string이라는 클래스 템플릿이나, 사용이 번거로워서 using을 이용해서 string으로 사용할 수 있게 만들었다.

template <class _Elem, class _Traits = char_traits<_Elem>, class _Alloc = allocator<_Elem> >
class basic_string

using string = std::basic_string<char, std::char_traits<char>, std::allocator<char> >

 

People1에 대한 어셈블리 코드

- Compiler Explorer 로 확인해보면, People1에서는 string에 대한 디폴트 생성자와 대입 연산자를 부르는 것이 확인된다.

- 그리고, basic_string의 소멸자가 불리는 것도 확인이 가능하다.

People2에 대한 어셈블리 코드

- People2 쪽이 훨씬 더 간단하게 끝나는 것이 확인가능하다.

※ 물론, 최근 컴파일러는 이것에 대한 최적화가 상당히 잘 이루어져서 People1도 효율적인 코드가 나올 수 있겠지만, People2 쪽이 일반적으로 훨씬 더 효율적인 기계어 코드를 만들 수 있다는 것을 의미한다.

※ 정리하면, 생성자에서 멤버 데이터를 초기화 할때는 멤버 초기화 리스트(member initializer list)를 사용하는 것이 좋다.


3. Member initializer list의 사용 예시

 

① 상수와 참조 변수가 멤버 데이터로 있는 경우

class Object
{
    const int c;
    int& r;
public:
    Object( int n, int& x)
    {
        c = n;
        r = x;
    }
};
int main()
{
    int num = 10;
    
    Object obj(10, num);
}

- 상수와 참조 변수는 반드시 초기화를 진행해야 한다. 생성자 블록 안에 대입했을때 상황은 아래와 같다.

const int c;
int& r;
c = n; // 상수는 대입 될 수 없다.
r = x; // 참조도 이는 동일하다.

- 따라서, 멤버 초기화 리스트를 사용해야 한다.

class Object
{
    const int c;
    int& r;
public:
    Object( int n, int& x) : c{n}, r{x}
    {
    }
};
int main()
{
    int num = 10;
    
    Object obj(10, num);
}

 

② 디폴트 생성자가 없는 타입이 멤버로 있는 경우

class Point
{
	int x;
	int y;
public:
	Point(int a, int b) : x{a}, y{b}
	{
	}
};

class Rect
{
	Point leftTop;
	Point rightBottom;
public:
	Rect(int x1, int y1, int x2, int y2)
	{
	}
};

int main()
{
	Rect r(1,1,10,10);
}

- 현재 Point 클래스는 디폴트 생성자가 없는 상태이다.

- 기본적으로 객체를 생성할땐 모든 멤버 데이터의 생성자도 호출된다.

Point leftTop;
Point rightBottom;

- 위 부분은 누가봐도 디폴트 생성자의 호출로 보인다.

- 이때, 특정 객체를 만들때 '어떻게 초기화' 할 지를 명시해 줄 수 있도록 멤버 초기화 리스트를 사용하면 된다.

Rect(int x1, int y1, int x2, int y2) : leftTop(x1, y1), rightBottom{x2,y2}

- 생성자를 호출시 멤버 데이터에 있는 객체를 어떻게 초기화 해줄지 명시가 가능하다.

- 괄호,중괄호 표기는 둘다 사용 가능함을 보여주기 위함이지만 일관성 있게 하는것이 좋다.

 

※ Member initializer list를 사용해야 하는 경우는 크게 2가지라고 할 수 있다.

① 상수와 참조 변수가 멤버 데이터로 있는 경우

② 디폴트 생성자가 없는 타입이 멤버로 있는 경우


4. Member initializer list의 사용시 주의 사항

 

① 멤버 초기화 리스트 코드에서는 멤버 데이터가 선언된 순서대로 초기화를 진행하라.

class Point
{
public:
    int x;
    int y;
    
    Point() : y{10}, x{y} // 미정의(undefined) 동작 발생.
    {
    }
}

- 멤버 데이터 순서상 x가 먼저 정의되어 있기 때문에 초기화 코드가 놓인 순서가 아닌 멤버 데이터가 선언된 순서대로 초기화가 된다. 즉, y가 먼저 입력되었지만, x가 먼저 선언되었기에 x먼저 초기화 하고 위 행위는 잘못된 행위이다.

- 멤버 초기화 리스트의 코드를 작성시에는 멤버 데이터가 선언된 순서대로 초기화 코드를 작성하는게 좋다.

 

선언과 구현 파일을 나눌때 유의 사항

// Point.h
class Point
{
    int x;
    int y;
public:
    Point(int x, int y);
};

// Point.cpp
#include "Point.h"

Point::Point(int x, int y) : x{x}, y{y}
{
// this->x = x;
}

- 멤버 초기화 리스트는 구현 파일에 작성 해야 한다.

- 함수(생성자)의 인자 이름이 멤버 데이터의 이름과 동일한 경우, 생성자 블록 안에서는 멤버를 접근하려면, this->멤버 이름을 사용해야하지만, 멤버 초기화 리스트 코드에서는 this가 필요없다. this는 별도 포스트로 다루고자 한다.


5. Default member initializer (C++11)

#include <iostream>

class Point
{
public:
	int x{0}; 
	int y{0};

	Point() {}
	Point(int a)        : x{a} {}
	Point(int a, int b) : x{a}, y{b} {}
};

int main()
{
	Point pt1;     // 0, 0
	Point pt2(1);  // 1, 0

	std::cout << pt1.x << ", " << pt1.y << std::endl;
	std::cout << pt2.x << ", " << pt2.y << std::endl;

}

- C++11부터 멤버 데이터에 초기값 지정이 가능하다.

- 원리는 컴파일러가 사용자가 위와 같이 만든 코드를 아래와 같이 변경하는 것이다.

#include <iostream>

class Point
{
public:
	int x{0}; 
	int y{0};

	Point()             : x{0}, y{0} {}
	Point(int a)        : x{a}, y{0} {}
	Point(int a, int b) : x{a}, y{b} {}
};

- 즉, 변환의 과정이 일어남을 유의해야 한다.

 

#include <iostream>

int g = 0;

class Point
{
public:
//	int x{0}; // int x = 0;
	int x{++g}; // 예시를 보여주기 위한 것이므로 절대 사용하면 안됨.
	int y{0};

	Point() {}
	Point(int a)        : x{a} {}
	Point(int a, int b) : x{a}, y{b} {}
};

int main()
{
	Point pt1;     // 1, 0
	Point pt2(1);  // 1, 0

	std::cout << pt1.x << ", " << pt1.y << std::endl;
	std::cout << pt2.x << ", " << pt2.y << std::endl;

	std::cout << g << std::endl; // 1
}

- 본 예제는 컴파일러가 Default member initializer를 어떤식으로 변환하는지 보여주는 과정이다. 

- 실제 코드는 가급적 이렇게 작성하진 말자. 아래와 같이 변환이 된다.

#include <iostream>

int g = 0;
class Point
{
public:
	int x{g++}; 
	int y{0};

	Point()             : x{g++}, y{0} {}
	Point(int a)        : x{a}, y{0} {}
	Point(int a, int b) : x{a}, y{b} {}
};
int main()
{
	Point pt1;     // 0, 0
	Point pt2(1);  // 1, 0

	std::cout << pt1.x << ", " << pt1.y << std::endl;
	std::cout << pt2.x << ", " << pt2.y << std::endl;

	std::cout << g << std::endl; // 1
}

- pt1의 경우는 첫번째 생성자가 호출 될 것이고, 이에 따라서 pt1의 x는 0이지만 g의 값은 1이 증가한다.

- 반면 pt2는 두번째 생성자가 호출되기 때문에, g의 값은 증가하지 않고 1이 되는것을 확인할 수 있다.

'C++ > Basic' 카테고리의 다른 글

[C++] Static Member  (0) 2024.08.16
[C++] Explicit Constructor  (0) 2024.08.14
[C++] Vector I  (0) 2024.08.13
[C++] Constructor / Destructor  (0) 2024.08.13
[C++] OOP(Object Oriented Programming) I  (0) 2024.08.12
Comments