
생성자
클래스 타입이 집합체(aggregate)라면 집합 초기화를 사용하여 클래스 타입 객체의 멤버 데이터를 초기화 할 수 있다
struct Foo
{
int a{};
int b{};
};
Foo foo{ 10, 20 }; //집합 초기화
집합 초기화는 멤버별 초기화를 수행하며 정의된 순서대로 초기화 된다
하지만 aggregate data type의 조건을 보면 멤버 데이터가 private:에 있다면 해당 클래스 타입은 더 이상 집합체가 아니게 된다, 따라서 집합 초기화를 사용할 수 없다
class Foo
{
int a{};
int b{};
};
Foo foo{ 10, 20 }; //class Foo는 집합체가 아니기 때문에 error
이렇게 멤버 데이터가 private:에 있으면 집합 초기화를 사용하지 못하는 합리적인 이유는 다음과 같다고 생각한다
그렇다면 이러한 private: 멤버 데이터는 선언과 동시에 어떻게 초기화를 해야할까? 바로 생성자를 사용하면 된다
생성자란 클래스 타입 객체가 생성된 후 자동으로 호출되는 특별한 멤버 함수이다
클래스 타입 객체를 생성하는 시점에서 컴파일러는 제공한 초기화값에 맞는 생성자가 있는지 확인하고 있다면 객체에 대한 메모리가 할당되고 생성자 함수를 호출한다, 만약 초기화값에 맞는 생성자를 찾지 못하면 컴파일 에러를 발생시킨다
여기서 중요한 점은 생성자가 객체를 생성하는게 아니라는 것이다, 생성자 호출전에 컴파일러가 객체에 대한 메모리 할당을 하고 생성자를 호출한다, 물론 초기화 값과 일치하는 생성자가 없다면 객체 생성 자체가 방지된다
보통 생성자에서는 멤버 변수 초기화, 초기화 값 오류 검사, file이나 db를 여는 작업등을 진행한다
집합체는 사용자 정의 생성자를 가질 수 없으며 사용자 정의 생성자를 만들게 된다면 더 이상 집합체가 아니게 된다
생성자 규칙
class Knight
{
public:
Knight(int inHp, int inMp)
{
std::cout << "Knight constructor called" << std::endl;
}
private:
int hp{};
int mp{};
};
int main()
{
Knight k{ 100, 200 }; //생성자 호출되기때문에 Knight constructor called가 출력된다
Knight k(100, 200); //생성자 호출되기때문에 Knight constructor called가 출력된다
return 0;
}
생성자도 마찬가지로 함수이기 때문에 인자로 넘긴 값이 매개변수 타입으로 암묵적 변환이 일어날 수 있다
class Knight
{
public:
Knight(int a, int b);
};
Knight k{ 'a', true }; //암묵적 변환 가능
Knight k(100.5f, 200); //암묵적 변환 가능
Knight k{ 100.5f, 200 }; //error -> { }초기화이기 때문에 데이터 손실이 발생할 수 있는 암시적 축소 변환이 불가능하다
또한 일반 함수와 마찬가지로 생성자 매개변수의 오른쪽 끝은 default값을 가질 수 있다
class Knight
{
public:
Knight(int a, int b = 100)
{
std::cout << a << b << std::endl;
}
private:
int hp{};
int mp{};
};
int main()
{
Knight K{20}; //20 100이 출력됨 (default value 100이 들어감)
return 0;
}
또한 생성자는 const가 될 수 없다, 멤버 데이터를 초기화하여 변경이 가능해야 하기 때문이다
class Knight
{
public:
Knight()
{
hp = 100; //const라면 이런식으로 멤버 데이터 변경이 불가능함
}
private:
int hp{};
int mp{};
};
일반적으로 non const 멤버 함수는 const 객체에서 호출이 불가능하다, 하지만 생성자는 예외이다, C++표준에 따르면 생성중인 객체에는 const가 적용되지 않는다
const Knight k{}; //그래서 non const인 생성자 함수가 const 객체인 k로부터 호출이 가능함
보통 생성자에서는 객체 전체 멤버 데이터를 인스턴스화 시점에 초기화 할 때 사용하고 setter는 단일 멤버 데이터를 초기화 할 때 사용하는게 좋다
생성자 멤버 초기화 리스트
생성자에서 멤버 데이터를 초기화 하는 방법으로 생성자 멤버 초기화 리스트를 사용하는 방법이 있다
class Knight
{
public:
Knight() : hp{ 100 }, mp{ 200 } //멤버 초기화 리스트
{
}
private:
int hp{};
int mp{};
};
이렇게 생성자 매개변수 뒤에 :로 시작해서 멤버 초기화 리스트를 할 수 있다, =를 이용한 복사 초기화는 사용할 수 없으며 ;으로 끝나지 않는다
class타입 객체가 인스턴스화 될 때 멤버 초기화 리스트에 있는 값들로 멤버 데이터가 초기화 된다
개인적으로 선호하는 format은 다음과 같다
Knight()
: hp{ 100 }, mp{ 200 }
{
}
멤버 초기화 리스트가 진행될 때 초기화 리스트의 순서대로 초기화 되는게 아니라 실제 멤버 데이터가 정의된 순서대로 초기화 된다 (mp를 먼저 쓴다고 mp가 hp보다 먼저 초기화 되는게 아님, hp가 mp보다 위에서 정의되었기 때문에)
class Foo
{
private:
int m_x{};
int m_y{};
public:
Foo(int x, int y)
: m_y { std::max(x, y) }, m_x { m_y }
{
}
void print() const
{
std::cout << "Foo(" << m_x << ", " << m_y << ")\n";
}
};
int main()
{
Foo foo { 6, 7 };
foo.print();
return 0;
}
위 코드의 결과값은 이상한 값으로 나오게 된다, 이유는 바로 m_y가 먼저 초기화 되지 않고 m_x가 초기화 되는데 m_y에는 초기화 되지 않은 값이 들어있었기 때문이다
따라서 실제 멤버 데이터의 정의 순서와 멤버 초기화 리스트의 순서를 맞춰주는게 좋다, 이로써 멤버 데이터를 다른 멤버 데이터로 초기화 해도 의존성에 의한 문제가 발생할 확률이 적어진다
멤버 데이터에 기본 초기화자가 있고 멤버 초기화 리스트도 있다면 멤버 초기화 리스트 값이 우선시 된다
class Knight
{
int hp{};
Knight()
: hp{ 100 }
{
}
};
hp멤버 데이터는 {}로 0으로 기본 초기화가 되지만 멤버 초기화 리스트가 100으로 초기화 되기 때문에 hp는 100이 된다
만약 기본 초기화자도 없고 멤버 초기화 리스트에서도 초기화 되지 않는다면 해당 변수에는 이상한 값이 들어있게 된다
생성자는 보통 멤버 초기화를 위해 사용되고 멤버 초기화 리스트에서 해당 초기화 작업을 하기 때문에 보통 생성자 body는 비어있게 되는 상황이 많다, 이러한 생성자 body에서 file이나 db등을 열거나 메모리 할당 작업을 보통 하게 된다
정리하면 멤버 초기화 리스트가 실행을 마치면 객체가 초기화 된 것으로 간주되고 생성자 함수 본문이 실행을 마치면 객체가 생성된 것으로 간주한다
그리고 생성자 body에서 =으로 값을 대입하는건 지양한다 (초기화가 아닌 대입이기 때문에)
멤버 초기화 리스트에서는 해당 초기화 값에 대한 validate를 검사하지 않는다, 따라서 보통 생성자 body에서 이러한 data validate를 체크한다 (assert, static_assert 등 아니면 try catch 등)
기본 생성자 (default constructor)
기본 생성자는 인자를 받지 않는 생성자이다 (매개변수가 없는 생성자)
class Knight
{
public:
Knight(); //기본 생성자
};
Knight k{}; //기본 생성자 호출 (초기화 값 없음)
Knight k; //기본 생성자 호출 (하지만 값 초기화 해주는게 더 좋다)
물론 여기서 Knight k; 만 해도 기본 생성자가 호출이 되지만 {}로 초기화 해주는게 더 좋다
생성자는 함수이기 때문에 오버로딩이 가능하다, 매개변수의 개수 혹은 매개변수의 타입이 다르다면 같은 이름의 생성자를 여러개 정의할 수 있다
class Knight
{
public:
Knight() //기본 생성자
{
std::cout << "Knight()" << '\n';
}
Knight(int a, int b) //기타 생성자
: hp{ a }
{
}
private:
int hp{};
};
Knight k{}; //기본 생성자 호출
Knight k{ 100, 200 }; //기타 생성자 호출
기본 생성자는 단 한개만 가질 수 있으며 여러개가 될 경우 컴파일러는 어떤 기본 생성자를 사용해야 할 지 모호해지기 때문에 컴파일 에러가 발생한다
단 이때 기타 생성자의 매개변수에 모든 값에 default값이 있다면 기본 생성자와 해당 기타 생성자 사용이 모호해져 컴파일 에러가 발생하게 된다
Knight(); //기본 생성자
Knight(int a = 100); //기타 생성자
Knight k{}; //기본 or 기타?? compile error
클래스 타입 객체에 프로그래머가 명시적으로 선언한 생성자가 없다면 컴파일러는 암묵적 기본 생성자 (implicit default constructor)를 생성한다
class Knight
{
private:
int hp{};
};
Knight k{}; //컴파일러가 생성한 암묵적 기본 생성자 호출
이렇게 컴파일러가 생성한 암묵적 기본 생성자는 객체를 인스턴스화 하는데 사용된다
Knight();
이거와 동일하다
또한 컴파일러에게 기본 생성자를 생성하라고 요청할 수 있다
class Knight
{
public:
Knight() = default; //명시적으로 기본 생성자 생성
Knight(int a, int b);
};
원래라면 Knight(int a, int b); 기타 생성자가 있기 때문에 기본 생성자가 호출되지 않지만 명시적으로 컴파일러에게 생성 요청을 한 것이다
이때 클래스가 사용자 정의가 아닌 기본 생성자 (암묵적 기본 생성자나 =default 로 정의된 기본 생성자)는 객체가 0으로 초기화 된다, 하지만 사용자 정의 기본 생성자를 비워두면 0으로 초기화 되지 않는다
class User
{
private:
int m_a;
int m_b {};
public:
User() {} // 사용자 정의 빈 생성자
int a() const { return m_a; }
int b() const { return m_b; }
};
class Default
{
private:
int m_a;
int m_b {};
public:
Default() = default; // 명시적 기본 생성자
int a() const { return m_a; }
int b() const { return m_b; }
};
class Implicit
{
private:
int m_a;
int m_b {};
public:
// 암묵적 기본 생성자
int a() const { return m_a; }
int b() const { return m_b; }
};
int main()
{
User user{}; //0으로 초기화 되지 않음
std::cout << user.a() << ' ' << user.b() << '\n';
Default def{}; // 0 초기화
std::cout << def.a() << ' ' << def.b() << '\n';
Implicit imp{}; // 0 초기화
std::cout << imp.a() << ' ' << imp.b() << '\n';
return 0;
}
따라서 user의 값만 쓰레기 값이 나오고 나머지는 0으로 초기화가 된 값이 나오게 된다
C++20 이전에는 =default로 만든 명시적 기본 생성자가 있는 경우에는 해당 클래스 타입이 집합체로 인정 받았지만 C++20 이후에는 비집합체로 처리된다
기본 생성자는 클래스 타입 객체를 프로그래머가 제공한 초기화 값 없이 생성할 수 있게 하기 때문에 해당 클래스 타입 객체의 멤버 데이터가 초기화 값 없이도 의미가 있을때 기본 생성자를 제공하는걸 권장한다