생성자란 무엇인가
이전 페이지에서는 클래스를 정의하고 멤버 변수를 부여했습니다. 하지만 갓 생성된 객체의 멤버는, 여러분이 값을 설정하지 않는 한 그 메모리에 있던 쓰레기 값을 그대로 담고 있습니다. 생성자가 이를 해결합니다. 생성자는 객체가 생성되는 순간 자동으로 실행되는 특별한 멤버 함수이며, 그 유일한 역할은 객체를 유효하고 완전히 초기화된 상태로 남겨 두는 것입니다.
생성자는 클래스와 이름이 같고 반환 타입이 없습니다 — void조차 없습니다. 여러분이 직접 호출하는 일은 없으며, 객체가 존재하게 될 때마다 컴파일러가 대신 호출해 줍니다.
매개변수가 없는 Counter()를 기본 생성자라고 부릅니다 — 인자를 전혀 전달하지 않고 객체를 생성할 때 사용되는 것입니다.
매개변수 생성자
인자를 받지 않는 생성자도 괜찮지만, 보통은 특정 값을 가진 객체를 생성하고 싶을 것입니다. 매개변수 생성자는 인자를 받아 그 값으로 멤버를 초기화합니다.
클래스는 매개변수 목록이 서로 다르기만 하면 여러 개의 생성자를 가질 수 있습니다. 이는 생성자에 적용된 일반적인 함수 오버로딩입니다. 여기서 Point는 좌표를 주거나 주지 않고 생성할 수 있습니다.
흔한 함정: Point p();는 객체를 생성하지 않습니다 — 컴파일러는 이를 Point를 반환하는 p라는 이름의 함수 선언으로 읽습니다. 기본 생성자를 호출하려면 Point p;(괄호 없이) 또는 중괄호를 쓴 Point p{};로 작성하세요.
멤버 초기화 리스트
지금까지의 예제는 생성자 본문 안에서 멤버에 대입했습니다. 단순한 타입에서는 동작하지만, 적절한 방법은 아닙니다. 본문이 실행될 때쯤이면 모든 멤버는 이미 기본 생성되어 있고, 본문은 그것을 버리고 그 위에 대입하는 셈입니다. 멤버 초기화 리스트는 본문보다 먼저, 각 멤버를 한 단계로 직접 초기화합니다.
문법은 매개변수 목록 뒤에 콜론을 붙이고, 이어서 member(value) 쌍을 나열하는 것입니다.
string 멤버의 경우, 이렇게 하면 빈 문자열을 만든 뒤 대입하는 것도 피할 수 있습니다 — 초기화 리스트가 첫 시도에서 곧바로 올바르게 만들어 줍니다.
초기화 리스트는 단순한 최적화가 아닙니다. 본문은 대입만 할 수 있을 뿐 초기화는 할 수 없기 때문에, 다음 세 가지 경우에는 필수입니다.
const멤버 —const는 일단 존재하고 나면 대입할 수 없습니다.- 참조 멤버 — 참조는 태어나는 순간 바인딩되어야 합니다.
- 타입에 기본 생성자가 없는 멤버.
class Sensor {
const int id; // const 멤버
int& slot; // 참조 멤버
public:
Sensor(int sensorId, int& s) : id(sensorId), slot(s) {}
// id나 slot을 본문에서 설정하려고 하면 컴파일되지 않는다.
};
알아 둘 미묘한 점: 멤버는 초기화 리스트에 나열한 순서가 아니라, 클래스에서 선언된 순서대로 초기화됩니다. 한 멤버의 초기화식이 다른 멤버를 읽는다면 중요한 것은 선언 순서이며, 이 둘을 혼동하는 것은 아직 초기화되지 않은 값을 사용하게 되는 전형적인 원인입니다.
기본 인자와 위임 생성자
항상 별도의 오버로드가 필요한 것은 아닙니다. 기본 인자를 사용하면 하나의 생성자로 여러 경우를 처리할 수 있습니다 — 인자를 생략하면 기본값이 채워 줍니다.
기본값을 가진 매개변수 생성자를 별도의 Point() 기본 생성자와 함께 두는 것은 조심해야 합니다 — 컴파일러는 Point p;에 대해 어느 쪽을 호출해야 할지 알 수 없어 모호성 오류를 보고합니다. 한 가지 방식을 선택하세요.
공통 설정을 공유하는 여러 생성자가 있을 때, 위임 생성자(C++11)를 사용하면 로직을 반복하는 대신 한 생성자가 다른 생성자를 호출할 수 있습니다. 다른 생성자를 초기화 리스트에 넣어 "위임"합니다.
복사 생성자
어떤 객체를 다른 객체의 복사본으로 생성할 때 — 값으로 전달하거나, 반환하거나, Foo b = a;라고 쓸 때 — 복사 생성자가 실행됩니다. 그 시그니처는 같은 타입에 대한 const 참조를 받습니다.
ClassName(const ClassName& other);
직접 작성하지 않으면, 컴파일러가 각 멤버를 복사하는 기본 복사 생성자를 생성합니다. 값(int, string, vector)만 담는 클래스라면 그것이 바로 올바른 동작이므로, 직접 작성해서는 안 됩니다.
가장 큰 함정은 메모리를 다루는 다음 장에 있습니다. 클래스가 힙 메모리를 가리키는 원시 포인터를 소유하고 있으면, 기본 복사 생성자는 데이터가 아니라 포인터를 복사합니다 — 그래서 두 객체가 같은 메모리를 가리키게 되고, 둘 다 그것을 해제하려 합니다. 이것이 이중 해제(double-free) 버그입니다. 경험칙은 _삼/오의 법칙_입니다. 사용자 정의 소멸자를 작성한다면, 거의 틀림없이 사용자 정의 복사 생성자(그리고 복사 대입)도 필요합니다. 현대 C++에서는 std::vector나 스마트 포인터를 보유하여 컴파일러가 생성한 복사가 그대로 잘 동작하게 하는 것이 더 깔끔한 해결책입니다.
또한 매개변수를 참조로 받는 것은 선택이 아니라 필수임에 유의하세요. 인자를 값으로 받는 복사 생성자는 자기 자신을 호출하기 위해 인자를 복사해야 하는데 — 이는 무한 재귀가 되어 애초에 컴파일조차 되지 않습니다.
다음: 소멸자
생성자가 객체를 세워 놓는다면, 소멸자는 그것을 해체합니다. 객체가 스코프를 벗어나거나 삭제되면 소멸자가 자동으로 실행됩니다 — 객체가 쥐고 있던 파일, 네트워크 연결, 힙 메모리를 해제하기에 완벽한 자리입니다. 다음 페이지에서는 소멸자가 어떻게 동작하는지, 정확히 언제 실행되는지, 그리고 C++의 강력한 RAII 패턴을 만들어 내기 위해 생성자와 어떻게 짝을 이루는지를 다룹니다.
자주 묻는 질문
C++에서 생성자란 무엇인가요?
생성자는 클래스와 이름이 같고 반환 타입이 없는 특별한 멤버 함수입니다. 객체가 생성될 때 자동으로 실행되며, 다른 코드가 객체를 사용하기 전에 그 객체를 유효하고 완전히 초기화된 상태로 만드는 것이 역할입니다.
기본 생성자와 매개변수 생성자의 차이는 무엇인가요?
기본 생성자는 인자를 받지 않으며, 값을 주지 않고 객체를 생성할 때(Point p;) 사용됩니다. 매개변수 생성자는 인자를 받아, 호출하는 쪽이 특정 값으로 객체를 초기화할 수 있게 합니다(Point p(3, 4);). 생성자는 매개변수 목록으로 오버로딩되므로, 클래스는 둘 다 가질 수 있습니다.
C++에서 멤버 초기화 리스트를 사용해야 하는 이유는 무엇인가요?
멤버 초기화 리스트(: name(n), age(a))는 생성자 본문이 실행되기 전에 멤버를 직접 초기화합니다. const 멤버, 참조, 기본 생성자가 없는 멤버에는 반드시 필요하며, 본문 안에서 대입할 때 생기는 낭비, 즉 기본 생성 후 다시 대입하는 동작을 피할 수 있습니다.