생성자
객체의 생성과 동시에 멤버 변수를 초기화해주는 멤버 함수
멤버 변수 초기화가 중요한 이유
Ex) 멤버 변수 초기화를 하지 않은 경우
#include <iostream>
using namespace std;
class Summ {
private:
int AccuSum;
public:
void sum(int a);
};
void Summ::sum(int a) {
AccuSum += a;
cout << "누적 합: " << AccuSum << endl;
}
int main() {
Summ Sum;
Sum.sum(3);
Sum.sum(7);
return 0;
}
누적 합: -858993457
누적 합: -858993450
- 멤버 변수를 초기화하지 않으면, 위 예제처럼 쓰레기 값이 출력된다.
- 클래스는 멤버 변수를 자동으로 0으로 초기화해주지 않기 때문이다.
Ex) 멤버 변수를 초기화한 경우
#include <iostream>
using namespace std;
class Summ {
private:
int AccuSum;
public:
void reset(); // 멤버 변수 초기화함수
void sum(int a);
};
void Summ::reset() {
cout << "값을 초기화하였습니다." << endl;
AccuSum = 0;
}
void Summ::sum(int a) {
AccuSum += a;
cout << "누적 합: " << AccuSum << endl;
}
int main() {
Summ Sum;
Sum.reset(); // 멤버 변수 초기화
Sum.sum(3);
Sum.sum(7);
return 0;
}
값을 초기화하였습니다.
누적 합: 3
누적 합: 10
- 멤버 변수를 초기화하는 함수 reset()을 생성함으로써 정상적으로 코드가 동작하였다.
- 만약 생성 후 초기화라는 과정을 잊어버리고, 초기화 함수를 구현하지 않는다면 프로그램 오류가 발생할 것이다. ▶ C++에서는 이러한 초기화 과정을 도와주는 함수인 생성자(Constructor)를 제공한다.
생성자 멤버 함수
- 생성자 함수는 객체 생성시 자동으로 호출되는 함수이며, 호출되면서 객체를 초기화한다.
- 생성자는 다음과 같이 정의한다.
/*클래스명*/ (/*인자들*/) {}
// Example
Pet(int _weight, int _age) {
weight = _weight;
age = _age;
}
생성자 구현 예제
#include <iostream>
using namespace std;
class Summ {
private:
int AccuSum;
public:
void sum(int a);
Summ(int _sum) { // 생성자 정의
AccuSum = _sum;
}
};
void Summ::sum(int a) {
AccuSum += a;
cout << "누적 합: " << AccuSum << endl;
}
int main() {
Summ Sum(0); // 인스턴스 생성하면서 생성자에 들어가는 인자를 대입
Sum.sum(3);
Sum.sum(7);
return 0;
}
누적 합: 3
누적 합: 10
- 생성자를 이용하여, 인스턴스 생성과 동시에 멤버 변수를 초기화하였다.
- 따로 앞선 예제의 reset()과 같은 멤버 변수 초기화 함수를 정의하여 호출할 필요없다.
디폴트 생성자 (Default constructor)
- 우리는 사실 생성자를 구현하지 않았을 때도 생성자는 호출되었다. (호출은 되었지만 멤버 변수는 생성자에 의해 쓰레기값으로 초기화된 것이다.)
- 클래스에는 생성자를 명시적으로 정의하지 않아도 컴파일러가 자동으로 추가해주는 디폴트 생성자가 존재한다.
- 이러한 디폴트 생성자를 우리가 직접 정의하여, 생성자에 들어가는 인자를 따로 적지 않아도 디폴트 값으로 초기화되도록 설정할 수 있다.
디폴트 생성자 예제
Ex) 디폴트 생성자 구현
#include <iostream>
using namespace std;
class Summ {
private:
int AccuSum;
public:
void sum(int a);
Summ() { // 디폴트 생성자 정의
AccuSum = 0; // 바로 초기화
}
};
void Summ::sum(int a) {
AccuSum += a;
cout << "누적 합: " << AccuSum << endl;
}
int main() {
Summ Sum; // 인자를 따로 넣지 않음
Sum.sum(3);
Sum.sum(7);
return 0;
}
누적 합: 3
누적 합: 10
- 디폴트 생성자를 이용하여 따로 생성자 인자를 기입하지 않아도 멤버 변수를 초기화할 수 있다.
디폴트 키워드
- 우리가 생성자를 정의하지 않은 것인지, 디폴트 생성자를 사용하려고 하는 건지 정확히 알 수 없을 때가 있을 것이다.
- C++ 11 이후부터는 defalut 키워드를 이용하여 디폴트 생성자를 정의하겠다는 것을 컴파일러에게 명시적으로 알려줄 수 있다.
class Test {
public:
Test() = default; // 디폴트 생성자를 정의하여라
};
소멸자
- 동적으로 인스턴스 변수에 메모리를 할당하는 경우(heap 공간을 할당하는 경우)에는 메모리 누수(memory leak)가 일어나지 않도록 delete 키워드를 이용하여 인스턴스 변수에 할당된 메모리를 해제해야 한다.
클래스 동적 할당하고 해제하기
Ex) 잘못된 동적 메모리 해제
#include <iostream>
#include <string>
using namespace std;
class Date {
private:
int year;
int month;
int day;
char *name;
public:
void printDate();
Date(int _year, int _month, int _day, const char *_name) {
year = _year;
month = _month;
day = _day;
name = new char[strlen(_name) + 1];
strcpy_s(name, strlen(_name) + 1 ,_name);
}
};
void Date::printDate() {
cout << year << "년 " << month << "월 " << day << "일" << endl;
cout << name << endl;
}
int main() {
Date* date[2];
date[0] = new Date(2023, 1, 4, "2023-01-04"); // 동적 메모리 할당
date[1] = new Date(2022, 2, 2, "2022-02-02");
date[0]->printDate(); // 포인터이므로 -> 기호 이용
cout << endl;
date[1]->printDate();
delete date[0]; // 인스턴스 date[0]의 동적 메모리 해제
delete date[1]; // 인스턴스 date[1]의 동적 메모리 해제
return 0;
}
2023년 1월 4일
2023-01-04
2022년 2월 2일
2022-02-02
- 시작부터 예제 이름을 "잘못된 동적 메모리 해제"라고 적었다.
- 마지막에 delete를 이용하여 인스턴스의 동적 메모리를 해제하여 성공적인 코드 작성이라고 생각할 수 있다.
- 하지만, 위 코드는 인스턴스 변수 name의 동적 메모리를 해제하지 않는다.
- 인스턴스 변수 name의 동적 메모리를 해제하지 않은채 계속해서 인스턴스 변수를 생성하고 소멸시키면 메모리 누수(memory leak)가 심각하게 발생한다.
- 이를 해결하기 위해서 C++은 인스턴스 변수의 동적 메모리를 해제하는 소멸자(Destructor)를 제공한다.
- 소멸자는 다음과 같이 선언하고 정의한다.
~(클래스명);
// 선언
~Date();
// 정의
~Date::~Date(){
delete[] name;
}
Ex) 소멸자를 이용한 정확한 동적 메모리 해제
#include <iostream>
#include <string>
using namespace std;
class Date {
private:
int year;
int month;
int day;
char *name;
public:
void printDate();
Date(int _year, int _month, int _day, const char *_name) {
year = _year;
month = _month;
day = _day;
name = new char[strlen(_name) + 1];
strcpy_s(name, strlen(_name) + 1 ,_name);
}
~Date();
};
Date::~Date() {
if (name != NULL) { // 만약 name이 동적할당되어 있다면
delete[] name;
cout << "성공적으로 메모리를 해제하였습니다." << endl;
}
}
void Date::printDate() {
cout << year << "년 " << month << "월 " << day << "일" << endl;
cout << name << endl;
}
int main() {
Date* date[2];
date[0] = new Date(2023, 1, 4, "2023-01-04");
date[1] = new Date(2022, 2, 2, "2022-02-02");
date[0]->printDate(); // 포인터이므로 -> 기호 이용
cout << endl;
date[1]->printDate();
cout << endl;
delete date[0];
delete date[1];
return 0;
}
2023년 1월 4일
2023-01-04
2022년 2월 2일
2022-02-02
성공적으로 메모리를 해제하였습니다.
성공적으로 메모리를 해제하였습니다.
- ~Date()라는 소멸자를 정의하였고, 소멸자는 객체가 파괴될 때 호출된다. (생성자가 객체가 생성될 때 호출되는 것과 반대)
- 객체가 파괴될 때 ~Date() 소멸자가 호출되었고, 인스턴스 변수 name에 동적으로 할당된 메모리가 있는지 확인한 후, 할당되었다면 동적 메모리까지 모두 해제하도록 구현하였다.
- 사실 디폴드 생성자처럼 우리가 소멸자를 따로 정의하지 않아도 디폴트 소멸자가 컴파일러에 의해 생성된다. 따라서 굳이 소멸자가 필요없는 클래스는 따로 소멸자를 명시할 필요는 없다.
복사 생성자
- 자신과 같은 클래스 타입의 다른 객체에 대한 참조(reference)를 인수로 전달받아, 그 참조를 가지고 자신을 초기화하는 방법
- 복사 생성자를 이용한 대입은 깊은 복사(deep copy)를 통한 값이 복사이기 때문에, 복사 생성자를 통해 새롭게 생성되는 객체는 원본 객체와 같은 값을 갖지만 완전히 독립적인 메모리 공간을 갖는다.
- 복사 생성자는 다음의 상황에서 주로 사용된다.
1. 객체가 함수에 인수로 전달될 때
2. 함수가 객체를 반환값으로 반환할 때
3. 새로운 객체를 같은 클래스 타입의 기존 객체와 똑같이 초기화할 때
- 복사 생성자의 표준적인 정의는 다음과 같다.
클래스명(const 클래스명& 객체명); // 다른 클래스의 객체를 상수 레퍼런스로 받음
// Example
Date (const Date& date);
복사 생성자 예제
#include <iostream>
using namespace std;
class Date {
private:
int year;
int month;
int day;
public:
void printDate();
void printAddress();
Date(int _year, int _month, int _day) {
year = _year;
month = _month;
day = _day;
}
Date(const Date& date) {
year = date.year;
month = date.month;
day = date.day;
}
};
void Date::printDate() {
cout << year << "년 " << month << "월 " << day << "일" << endl;
}
void Date::printAddress() {
cout << &year << " " << &month << " " << &day << endl;
}
int main() {
Date date1(2023, 1, 4);
Date date2(date1); // 복사 생성자 이용하여 멤버 변수 초기화
Date date3 = date2; // Date date3(date2)와 동일한 문장
date1.printAddress();
date1.printDate();
cout << endl;
date2.printAddress();
date2.printDate();
cout << endl;
date3.printAddress();
date3.printDate();
return 0;
}
00CFF83C 00CFF840 00CFF844
2023년 1월 4일
00CFF828 00CFF82C 00CFF830
2023년 1월 4일
00CFF814 00CFF818 00CFF81C
2023년 1월 4일
- 인스턴스 date1을 상수 레퍼런스로 받아서 동일한 값을 갖는 인스턴스 date2, date3를 생성하였다.
- 복사 생성자에 의한 값 복사는 깊은 복사이기 때문에 인스턴스 변수들의 주소가 모두 다르다.
- 굳이 인스턴스 변수의 값을 복사할 목적이라면 const 키워드는 그대로 두는 것이 좋겠다.
디폴트 복사 생성자
- 사실 디폴트 복사 생성자가 존재해서, 따로 복사 생성자 부분을 작성하지 않아도 인스턴스 변수 값 복사가 가능하다.
- 그러나 컴파일러가 자동으로 만드는 디폴트 복사 생성자는 얕은 복사만 가능하다.
Ex) 디폴트 복사 생성자 예제
#include <iostream>
using namespace std;
class Date {
private:
int year;
int month;
int day;
public:
void printDate();
Date(int _year, int _month, int _day) {
year = _year;
month = _month;
day = _day;
}
};
void Date::printDate() {
cout << year << "년 " << month << "월 " << day << "일" << endl;
}
int main() {
Date date1(2023, 1, 4);
Date date2(date1); // 디폴트 복사 생성자
Date date3 = date2; // Date date3(date2)와 동일한 문장
date1.printDate();
date2.printDate();
date3.printDate();
return 0;
}
2023년 1월 4일
2023년 1월 4일
2023년 1월 4일
- 따로 복사 생성자를 정의하지 않아도 이전과 동일한 방법으로 값을 복사할 수 있다.
Ex) 디폴트 복사 생성자의 얕은 복사가 일으키는 오류
#include <iostream>
#include <string>
using namespace std;
class Date {
private:
int year;
int month;
int day;
char *name;
public:
void printDate();
void printAddress();
Date(int _year, int _month, int _day, const char *_name) {
year = _year;
month = _month;
day = _day;
name = new char[strlen(_name) + 1];
strcpy_s(name, strlen(_name) + 1 ,_name);
}
~Date() {
if (name) delete[] name;
}
};
void Date::printDate() {
cout << year << "년 " << month << "월 " << day << "일" << endl;
cout << name << endl;
}
void Date::printAddress() {
cout << &year << " " << &month << " " << &day << " " << reinterpret_cast<void *>(name) << endl;
} // name이 char형 포인터이기 때문에 형변환을 이용
int main() {
Date date1(2023, 1, 4, "2023-01-04");
Date date2(date1);
date1.printAddress();
date1.printDate();
cout << endl;
date2.printAddress();
date2.printDate();
cout << endl;
return 0;
}
007EFAA0 007EFAA4 007EFAA8 00BFF988
2023년 1월 4일
2023-01-04
007EFA88 007EFA8C 007EFA90 00BFF988
2023년 1월 4일
2023-01-04
- 멤버 변수 name이라는 동적 배열을 생성하였기 때문에 소멸자를 이용하여 변수의 동적 메모리를 해제하는 부분을 구현하였다.
- 그런데 코드를 실행하면 오류가 발생하는 것을 확인할 수 있다.
- 이는 디폴트 복사 생성자에 의해서 값을 복사하는 경우는, 단순히 값을 대입하는 얕은 복사이기 때문이다.
- 따라서 date1과 date2의 포인트 변수 name이 가리키는 값의 주소가 동일하다.
- 이렇게 같은 주소의 값을 가리키는 동안에 소멸자를 이용하여 동적 메모리(heap 공간)를 해제하는 경우 다음과 같은 문제가 발생한다.
- 소멸자에 의해 date1의 인스턴스 변수 name이 가리키는 heap 공간이 해제된다.
- 이것에 의해 date2의 인스턴스 변수 name은 가리켜야 하는 heap 공간이 사라져버린다.
- 따라서 이러한 경우에는 사용자 정의 복사 생성자를 통해 깊은 복사를 직접 구현하여 값을 대입하여야 한다.
Ex) 사용자 정의 복사 생성자로 깊은 복사 구현
#include <iostream>
#include <string>
using namespace std;
class Date {
private:
int year;
int month;
int day;
char *name;
public:
void printDate();
void printAddress();
Date(int _year, int _month, int _day, const char *_name) {
year = _year;
month = _month;
day = _day;
name = new char[strlen(_name) + 1];
strcpy_s(name, strlen(_name) + 1 ,_name);
}
Date(const Date& date) {
year = date.year;
month = date.month;
day = date.day;
name = new char[strlen(date.name) + 1]; // 복사 생성자에 깊은 복사를 구현한다.
strcpy_s(name, strlen(date.name) + 1, date.name);
}
~Date() {
if (name) delete[] name;
}
};
void Date::printDate() {
cout << year << "년 " << month << "월 " << day << "일" << endl;
cout << name << endl;
}
void Date::printAddress() {
cout << &year << " " << &month << " " << &day << " " << reinterpret_cast<void *>(name) << endl;
}
int main() {
Date date1(2023, 1, 4, "2023-01-04");
Date date2(date1);
date1.printAddress();
date1.printDate();
cout << endl;
date2.printAddress();
date2.printDate();
cout << endl;
return 0;
}
009BFBEC 009BFBF0 009BFBF4 00C5F640
2023년 1월 4일
2023-01-04
009BFBD4 009BFBD8 009BFBDC 00C5F480
2023년 1월 4일
2023-01-04
- date1과 date2의 인스턴스 변수 name이 가리키는 값의 주소가 다르기 때문에 오류가 발생하지 않는다.
생성자의 초기화 리스트 (initializer list)
- 초기화 리스트는 생성자 이름 뒤에 와서 생성자 호출과 동시에 멤버 변수들을 초기화하는 역할을 한다.
- 기존의 생성자 정의부분과 크게 다를 게 없어 보이지만 약간의 차이가 있다.
- 기존 방식은 생성을 먼저 한 후, 값을 대입한다. ▶ int a; → a = 10;
- 초기화 리스트를 이용하는 방식은 생성과 동시에 값을 초기화한다. ▶ int a = 10;
- 전자는 디폴트 생성자가 호출되고 대입이 수행되며, 후자는 복사 생성자가 호출된다. 하는 일도 후자가 더 적다.
- 레퍼런스 변수와 상수는 무조건 생성과 동시에 값을 초기화해야 하므로, 이를 생성하기 위해서는 꼭 초기화 리스트를 사용해야 한다.
초기화 리스트 정의
- 초기화 리스트의 형태는 다음과 같다.
/*클래스명*/::/*생성자명*/: 변수1(값1), 변수2(값2), 변수3(값3), ... {}
// example
Date::Date() : year(2023), month(1), day(6) {}
// 따로 지정할 인자가 있는 경우
Date::Date(int _year) : year(_year), month(1), day(6) {}
초기화 리스트 예제
#include <iostream>
using namespace std;
class Date {
private:
int year;
const int month; // 상수
const int day; // 상수
public:
void printDate();
Date(int _year);
~Date();
};
Date::Date(int _year) : year(_year), month(1), day(6) { cout << "객체 생성 및 초기화 완료." << endl; } // 초기화 리스트 정의
Date::~Date() {
cout << "객체가 소멸되었습니다." << endl;
};
void Date::printDate() {
cout << year << "년 " << month << "월 " << day << "일" << endl;
}
int main() {
Date* date;
date = new Date(2023);
date->printDate(); // 포인터이므로 -> 기호 이용
cout << endl;
delete date;
return 0;
}
객체 생성 및 초기화 완료.
2023년 1월 6일
객체가 소멸되었습니다.
- month와 day 변수를 const 키워드를 이용하여 상수 처리하였고, 초기화 리스트를 이용하여 값을 초기화하였다.
- 초기화 리스트는 클래스 멤버 변수 중에서 절대로 변하지 않아야 하는 값을 초기화할 때 유용히 사용된다.