객체가 생성(초기화)될 때 호출되는 함수가 생성자이다.
생성자는 해당 클래스 타입의 객체를 생성할 때마다 실행된다.
함수의 이름을 클래스와 이름을 같게 만들고 반환 타입을 명시하지않으면 생성자로 간주된다.
class Person
{
public:
Person()
{
}
private:
int _age;
std::string _name;
}
매개변수 목록이 다른 여러개의 생성자가 있을 수 있고 생성자는 const로 선언할 수 없다.
class Person
{
public:
Person()
{
}
Person(int age, std::string name)
{
_age = age;
_name = name;
}
Person(int age)
{
_age = age;
}
Person(std::string name)
{
_name = name;
}
private:
int _age;
std::string _name;
}
기본 생성자는 매개변수 목록이 비어있는 생성자를 말한다.
Person me;
Person you;
위와 같이 인자 없이 객체를 만들때 기본 생성자로 객체를 초기화한다.
어떠한 생성자도 선언하지 않으면 컴파일러는 암시적으로 기본 생성자를 만들어서 넣는다.
이를 합성(Synthesize)했다고 하고 이렇게 생성된 생성자는 합성 기본 생성자(Synthesized Default Constructor)라고 한다.
class Person
{
public:
/* 컴파일러가 만들어 넣은 합성 기본 생성자
Person()
{
}
*/
//...
}
대신 생성자를 하나라도 선언했다면 합성 기본 생성자는 합성되지 않는다.
class Person
{
public:
/*
선언/정의한 기본 생성자는 없다.
선언한 다른 생성자가 존재하여, 그러므로 컴파일러는 기본 생성자를 합성하지않는다.
*/
Person(int age, std::string name)
{
_age = age;
_name = name;
}
//...
}
합성한 것이든 직접 선언한 것이든 기본 생성자가 없을 시에는 당연히 아래와 같은 객체 생성은 할 수 없다.
컴파일러에게 명시적으로 합성 기본 생성자를 합성해달라고 할 때 사용할 수 있다.
class Person
{
public:
Person() = default;
Person(int age, std::string name)
{
_age = age;
_name = name;
}
//...
}
기본 생성자를 합성했으면 다음과 같은 객체 초기화가 가능해진다.
Person p; // ok
영어로는 Constructor Initializer List이다.
Person(int age, std::string name)
: _age(age), _name(name)
{
}
Person(int age)
: _age(age)
{
}
Person(std::string name)
: _name(name)
{
}
//...
이런 식으로 클래스 멤버변수들을 초기화할 수 있다.
그치만 일반적으로 클래스 내 초기 값을 사용하는 것이 가장 좋다.
class Person
{
public:
//...
private:
// 클래스 내 초기 값
int _age = 0;
std::string _name = "";
}
하지만 클래스 내 초기 값을 지원하지 않으면 모든 생성자에서 모든 내장 타입 멤버를 명시적으로 초기화해야 한다.
복사 생성자를 이야기하기 전에 복사에 대해 이야기 해보자.
위에서 예시로 든 Person이라는 클래스 객체를 복사하자.
Person me(28, "wondong");
Person me2 = me; // 첫번째 복사
Person me3;
me3 = me2; // 두번째 복사
첫번째 복사는 복사 생성자라는 생성자로 복사가 일어나고
두번째 복사는 이미 초기화된 객체에 복사를 하는 것이니 복사 대입 연산자로 복사가 일어난다.
복사 생성자? 저 구현안했는데요?
복사 생성자도 컴파일러가 합성하는 생성자 중에 하나이다.
복사 생성자와 복사 대입 연산자를 명시적으로 구현하지 않으면 컴파일러는 이를 또 합성하여 다음과 같은 정의의 생성자와 연산자를 실행한다.
class Person
{
public:
Person() = default;
Person(int age, std::string name)
: _age(age), _name(name)
{
}
/* 컴파일러가 합성한 복사 생성자와 복사 대입 연산자
Person(const Person& p)
{
_age = p._age;
_name = p._name;
}
Person& operator=(const Person& p)
{
_age = p._age;
_name = p._name;
return *this;
}
*/
//...
}
me2._age = me._age;
me2.name = me._name;
//...
me3._age = me2._age;
me3.name = me2._name;
그렇다. 복사 생성자와 복사 대입 연산자는 기본 생성자와는 다르게 다른 생성자를 정의하더라도 복사 생성자와 복사 대입 연산자를 정의하지 않았다면 컴파일러가 합성한다.
각 생성자들의 합성 조건을 알아두는 것이 좋다.
class Wallet
{
public:
Wallet() {};
Wallet(int money) : _money(money) {}
private:
int _money = 0;
friend class Person;
};
class Person
{
public:
Person() = default;
Person(int age, std::string name, Wallet* wallet)
: _age(age), _name(name), _wallet(wallet)
{
}
Person(int age, std::string name)
: _age(age), _name(name)
{
}
Person(int age)
: _age(age)
{
}
Person(std::string name)
: _name(name)
{
}
/* 컴파일러가 합성한 복사 생성자와 복사 대입 연산자
Person(const Person& p)
{
_age = p._age;
_name = p._name;
_wallet = p._wallet;
}
Person& operator=(const Person& p)
{
_age = p._age;
_name = p._name;
_wallet = p._wallet;
return *this;
}
*/
~Person()
{
if (_wallet != nullptr)
delete _wallet;
}
private:
int _age = 0;
std::string _name = "";
Wallet* _wallet = nullptr;
};
Wallet* _wallet = nullptr;
Wallet
포인터 타입의 멤버변수가 추가되었다.
이는 동적 할당해서 주소를 갖고 있는 변수이므로 객체가 소멸되면 해당 주소가 유효하면 메모리를 정리하도록 소멸자도 추가하였다.
~Person()
{
if (_wallet != nullptr)
delete _wallet;
}
이제부터 문제가 된다.
복사를 수행한 두 Person
이 하나의 Wallet
의 주소를 가지게 되므로 객체가 소멸될 때 서로 같은 Wallet
객체를 해제하려고 한다.
그래서 이럴땐 _wallet
멤버변수는 DeepCopy를 수행해줘야 한다.
Person(const Person& p)
{
std::cout << "Person(const Person& p)" << std::endl;
_age = p._age;
_name = p._name;
if(p._wallet)
_wallet = new Wallet(p._wallet->_money);
}
물론 두 Person이 하나의 Wallet을 가진다고 설계할 수도 있겠지만 이는 나중에 스마트 포인터로 해결해보도록 한다.
지금은 Person은 각각 서로 다른 Wallet 객체를 가진다고 가정하자.
위에서 언뜻 나왔지만
소멸자는 다음과 같이 정의할 수 있다.
~Person()
{
}
소멸자는 보통, 복사 생성자와 복사 대입 연산자를 명시적으로 구현할 필요가 있다면 같이 구현할 필요가 있다.
복사 대입 연산자는 다음과 같이 정의한다.
Person& operator=(const Person& p)
{
std::cout << "operator=(const Person& p)" << std::endl;
_age = p._age;
_name = p._name;
if (p._wallet)
_wallet = new Wallet(p._wallet->_money);
return *this;
}
함수의 본체는 복사 생성자와 비슷하게 가면 된다.
대신 *this
를 리턴하여 이어지는 연산에 *this
를 사용할 수 있게 한다.
객체의 복사를 제어하는 데는 세가지 기본 연산가 있는데, 복사 생성자, 복사 대입 연산자, 소멸자가 있다.
게다가 새로운 표준에서는 이동 생성자와 이동 대입 연산자(이는 다음 포스팅에서 살펴보기로 하고)가 있는데 여기까지 5개이다.
여기서 3/5 법칙이라는 말이 있는데, 3은 소멸자, 복사 생성자, 복사 대입 연산자이고, 5는 3의 세개와 이동 생성자, 이동 대입 연산자이다.
클래스에 복사 제어 멤버를 직접 정의해야 하는지 결정할 때 사용할 한 가지 원칙은 클래스에 소멸자가 필요한지 먼저 보는 것이다.
소멸자가 필요하면 복사 생성자와 복사 대입 연산자가 거의 꼭 필요하다.
이것이 3/5 법칙의 3이다.
복사 생성자만 필요하거나 복사 대입 연산자만 필요한 경우는 거의 없다.
둘 중 하나가 필요하다고 판단되면 나머지 하나도 필요한 경우가 대부분이다.
일반적으로는 객체의 복사를 금지하진 않겠지만 간혹 어떤 타입은 복사가 안됐으면 할 때가 있다.
예를 들어 iostream 클래스에서는 동일한 IO 버퍼에 여러 객체를 기록하거나 읽는 일이 없도록 복사를 금지한다.
복사 제어 멤버(복사 생성자, 복사 대입 연산자)를 정의하지 않으면 컴파일러가 암묵적으로 합성해주기 때문에 이를 금지하려면 다른 방법이 필요하다.
class BankAccount
{
public:
BankAccount()
{
//...
}
BankAccount(const BankAccount& b) = delete;
BankAccount& operator=(const BankAccount& b) = delete;
};
= delete
를 사용하여 명시적으로 컴파일러에게 합성을 하지 말도록 요청할 수 있다.
= delete
는 그외에도 함수 일치 과정을 유도하고 싶을 때에도 유용하다.
struct Z {
// ...
Z(long long); // can initialize with an long long
Z(long) = delete; // but not anything less
};
새로운 표준 이전에는 크래스에서 복사 생성자와 복사 대입 연산자를 private으로 선언해 복사를 금지했다.
class BankAccount
{
public:
BankAccount()
{
//...
}
private:
BankAccount(const BankAccount& b);
BankAccount& operator=(const BankAccount& b);
};
대신 이 방법은 friend 클래스들에서는 접근이 가능할 것이다.
멤버 함수를 선언만 하고 정의하지 않아도 된다.
대신 해당 함수의 사용처가 있다면 링크할 때 실패한다.