초기화 리스트에 대해 알아보자😓
안녕하세요! 두두코딩 입니다 ✋
오늘은 C++ 초기화에 대해 알아보겠습니다.
해당 내용은 Effective C++ 책을 참고해 작성하였습니다.
🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!
궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇
객체 초기화
C++ 항목에 대한 초기화를 생각해보자. 우리는 늘상 어떤 변수를 선언할 때, 초기화를 종종하곤 한다. 하지만, C++은 platform 따라 초기화 유무가 변해 머리를 아파게 할 때도 있다. 예제 코드를 보며 생각해보자.
int x; // 0 or undefined?
class Point {
int x, y;
};
Point p; // 0 or undefined?
위의 코드를 보면, 기본 타입인 int
형으로 선언을 하든, 사용자 정의타입 Point
로 하든 관계없이 초기화가 0으로 될 수도 있고 안될 수도 있다. 즉, 플랫폼에 따라 움직인다.
이런 경우를 그대로 나둘 경우 정의 되지 않은 동작이 그대로 흘러가게 되고, 훗날 프로그래머는 밤을 세고 있을지도 모른다.. 😵 C++의 객체 혹은 변수의 초기화가 중구난방 같지만, 절대 그런건 아니다. 규칙이 명확하게 존재하는데, 몇 가지 규칙들을 알아보자.
🌱 C++에서 C 영역만 사용하면서, runtime 시 초기화에 시간이 오래 걸릴 경우 초기화 보장이 되지 않는다. (기억할 것)
🌱 모든 객체를 초기화 할때, 초기화하는 습관을 가져야한다.
🌱 C++의 STL 부분 (e.g. vector)는 초기화가 된다.
🌱 사용자 정의 타입 (객체)의 초기화는 생성자에서 보장해야한다.
위의 규칙을 2가지로 크게 나눠보면, 사용자 정의 타입 초기화가 아닌 경우 초기화를 필수적으로 해야한다 라고 생각할 수 있다. (STL은 예외로 두자!)
기본 타입 초기화
int x = 0;
const char *text = "this is effective C++";
double d;
std::cin >> d;
위와 같이, 초기화를 하는 습관을 들이는 것이 중요하다. 그렇다면, 사용자 정의타입의 초기화는 어떤 방식으로 해야할까? 한번 알아보자.
초기화 리스트 사용
기본 타입의 초기화는 잘 지키기만 하면 된다. 하지만, 사용자 정의타입 초기화 같은 경우 대입 (assignment) 와 초기화 (initialization)을 구별해서 사용해야한다. 아래의 코드를 보자.
class PhoneNumber { ... };
class ABEntry {
public:
ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
{
this.theName = name; // 지금 모두 대입을 하고 있다.
this.theAddress = address; // 초기화가 아니다.
this.thePhones = phones;
numTimesConsulted = 0;
}
위의 코드를 보기에 앞서 C++의 규칙을 하나 알고가자.
💠 C++ 규칙에 의하면 어떤 객체이든 그 객체의 데이터 멤버는 생성자의 본문이 실행되기 전에 초기화 되어야 한다
위의 규칙을 기억하고, 해당 코드를 보자. 우리는 ABEntry
의 생성자를 만들어 초기화 했다. 얼핏 보면 복사 생성자로 초기화 했는데? 뭐가 문제지 라는 생각을 할 수 있다. 하지만, theName
, theAddress
, thePhones
의 사용자 정의 객체들의 생성자인 기본생성자 들이 먼저 불린 후, ABEntry
생성자 내에서 대입연산이 이뤄진다. 즉, 위의 C++ 규칙을 벗어나 코드가 작성되고 있는것을 알 수 있다. 우리는 “대입” 연산이 발생하기 전 초기화가 먼저 일어나도록 변경하기 위해 초기화 리스트 를 사용해야한다.
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
: theName(name), // 초기화 리스트에 들어간 멤버는
theAddress(address), // 초기화가 먼저 일어남.
thePhones(phones),
numTimeConsulted(0)
{}
위의 생성자는 초기화 리스트를 활용해 작성한 것이다. 데이터 멤버에 사용자가 원하는 값을 주고 시작한다는 점에서는 똑같지만, 방금 만든 생성자는 앞에 만든 생성자보다 더 효율적이다.
기존 생성자 같은 경우, theName
과 같은 멤버변수들이 기본생성자를 호출해 객체를
완성한 다음 대입연산을 수행한다. 즉, 2번의 동작을 수행한다. 하지만, 아래와 같이
초기화 리스트를 사용한 생성자는 기본생성자가 아닌
초기화 시점에 복사생성자를 호출해, 1번의 동작만 수행하면 된다. (더 효율적이다😎)
그렇다면, 사용자 정의 객체가 아닌 기본타입을 초기화 리스트에 넣어서 관리할까? 기본타입 같은 경우 초기화리스트를 활용하든, 대입하든 성능은 동일하다. 기본적으로 넣는 습관을 들여야 혹 빼먹을 수 있는 일이 발생하지 않을 수 있다. 만약 기본타입의 초기화를 빼먹을 경우 초기화가 될수도 있고 안될수도 있기 때문에 그냥 넣어주는 것을 습과화 하자.
기본타입 같은 경우 초기화 리스트에 넣는 일이 선택이 아닌 의무가 될 때가 있다. 상수 혹은 참조자로 되어있는 데이터멤버의 경우 반드시 초기화가 일어나야한다.
상수 와 참조자는 대입 자체가 불가능하기 때문에, 반드시 초기화를 먼저해야한다. 어떤 것은 하고 어떤 것은 안하고, 복잡하게 생각하지말고 초기화는 무조건 초기화 리스트를 통해 하는 습관을 들이자!
C++의 객체 초기화는 보는 것과 같이 꽤나 변덕스럽다. 그 중 변덕스럽지 않는 부분이 있는데, 바로 객체를 구성하는 초기화 순서 이다. 이 순서는 어떤 컴파일러를 막론하고, 항상 똑같다.
해당 부분은 다음 포스팅에서 살펴보도록하자! 한숨 돌리고 여기를 클릭해주기 바란다😤