도순씨의 코딩일지
C++ :: 생성자(Constructor)와 소멸자(Destructor) 본문
매개변수들을 초기하기 위해서 함수를 정의해주었는데, 매우 번거로운 일이었습니다. 이러한 불편함을 막기 위해서 생성자가 만들어졌습니다. 생성자를 이용하여 객체도 생성과 동시에 초기화할 수 있습니다.
🌼 생성자의 이해
생성자의 이해를 위해서 간단한 클래스 하나를 살펴보도록 합시다.
1
2
3
4
5
6
7
8
9
10
11
|
class SimpleClass{
private:
int num;
public:
SimpleClass(int n){ // 생성자
num = n;
}
int GetNum() const{
return num;
}
};
|
cs |
위 클래스 정의에서 SimpleClass라는 함수를 확인할 수 있습니다. 이 함수는 클래스와 이름이 동일하고 반환형이 선언되어 있지 않습니다. 이러한 유형의 함수를 생성자라고 부릅니다. 생성자는 객체 생성시 딱 한 번만 호출됩니다. 또한 생성자는 다음과 같은 특징을 가지고 있습니다.
💡 생성자도 함수의 일종이니 오버로딩이 가능하다.
💡 생성자도 매개변수에 '디폴트 값을 설정할 수 있다.
다음 예제를 통해 위 특징을 파악해보도록 합시다.
⭐️ Constructor1.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
#include <iostream>
using namespace std;
class SimpleClass{
private:
int num1;
int num2;
public:
SimpleClass(){
num1 = 0;
num2 = 0;
}
SimpleClass(int n){
num1 = n;
num2 = 0;
}
SimpleClass(int n1, int n2){
num1 = n1;
num2 = n2;
}
/*
* 디폴트 값을 설정가능하다
SimpleClass(int n1 = 0, int n2 = 0){
num1 = n1;
num2 = n2;
}
*/
void ShowData() const{
cout<<num1<<' '<<num2 << endl;
}
};
int main(void){
SimpleClass sc1;
sc1.ShowData();
SimpleClass sc2(100);
sc2.ShowData();
SimpleClass sc3(100, 200);
sc3.ShowData();
return 0;
}
|
cs |
⭐️ Constructor1.cpp 실행 결과
1
2
3
|
0 0
100 0
100 200
|
cs |
만약에 주석 처리를 한 부분을 주석 처리 하지 않고 실행한다면 어떻게 될까요? 같은 매개변수를 사용하는 생성자가 있습니다. 따라서 에러가 발생할 것입니다.
⭐️ Constructor2.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
#include <iostream>
using namespace std;
class SimpleClass{
private:
int num1;
int num2;
public:
SimpleClass(int n1 = 0, int n2 = 0){
num1 = n1;
num2 = n2;
}
void ShowData() const{
cout<<num1<<' '<<num2 << endl;
}
};
int main(void){
SimpleClass sc1();
SimpleClass mysc = sc1();
mysc.ShowData();
return 0;
}
SimpleClass sc1(){
SimpleClass sc(20, 30);
return sc;
}
|
cs |
⭐️ Constructor2.cpp 실행 결과
1
|
20 30
|
cs |
보통 함수의 원형은 전역적으로 선언하지만, 함수 내에 지역적으로도 선언이 가능합니다. 또한 19번째 라인의 코드는 함수의 원형 선언에 해당합니다. 이 문장을 void형 생성자의 호출문으로 인정해버리면, 컴파일러는 이것이 객체 생성문인지, 함수의 원형선언인지를 구분할 수 없습니다. 그래서 이런 유형의 문장을 함수의 원형 선언에만 사용하기로 약속하였습니다.
다음 코드를 통해 더 깊이있게 생성자 활용에 대해서 알아보도록 합시다.
⭐️ 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
#include <iostream>
using namespace std;
class FruitSeller{
private:
int APPLE_PRICE;
int numOfApples;
int myMoney;
public:
FruitSeller(int price, int num, int money){
APPLE_PRICE = price;
numOfApples = num;
myMoney = money;
}
int SaleApples(int money){
int num = money / APPLE_PRICE;
numOfApples -= num;
myMoney += money;
return num;
}
void ShowSalesResult() const{
cout << "남은 사과: " << numOfApples<<endl;
cout << "판매 수익: " << myMoney<<endl<<endl;
}
};
class FruitBuyer{
private:
int myMoney;
int numOfApples;
public:
FruitBuyer(int money){
myMoney = money;
numOfApples = 0;
}
void BuyApples(FruitSeller &seller, int money){
numOfApples += seller.SaleApples(money);
myMoney -= money;
}
void ShowBuyResult() const{
cout << "현재 잔액: " << myMoney << endl;
cout << "사과 개수: " << numOfApples << endl<<endl;
}
};
int main(void){
FruitSeller seller(1000, 20, 0);
FruitBuyer buyer(5000);
buyer.BuyApples(seller, 2000);
cout<<"과일 판매자의 현황"<<endl;
seller.ShowSalesResult();
cout << "과일 구매자의 현황"<<endl;
buyer.ShowBuyResult();
return 0;
}
|
cs |
⭐️ 실행 결과
1
2
3
4
5
6
7
|
과일 판매자의 현황
남은 사과: 18
판매 수익: 2000
과일 구매자의 현황
현재 잔액: 3000
사과 개수: 2
|
cs |
각각 클래스의 이름과 동일한 FruitSeller, FruitBuyer 생성자가 적용되었음을 알 수 있습니다.
하지만 만약 두 개 이상의 다른 객체를 멤버로 생성한다면 문제가 생길 수 있습니다. 예를 들어서 Rectangle 클래스가 두 개의 Point 객체를 멤버로 지니고 있다고 가정해봅시다. 즉, Rectangle 객체가 생성되면 두 개의 Point 객체가 함께 생성되는 것입니다. 따라서 다음과 같은 생각을 해 볼 수 있습니다.
"Rectangle 객체를 생성하는 과정에서 Point 클래스의 생성자를 통해서 Point 객체를 초기화할 수 없을까?"
다행히 멤버 이니셜라이저(member initializer)라는 것을 사용하면 우리가 원하는 것을 할 수 있습니다. 상수멤버를 초기화할 수 있게 하는 것이죠.
🌼 멤버 이니셜라이저(Member Initializer)를 이용한 멤버 초기화
다음은 생성자가 추가된 Rectangle 클래스의 선언입니다.
1
2
3
4
5
6
7
8
|
class Rectangle{
private:
Point upLeft;
Point lowRight;
public:
Rectangle(const int &x1, const int &y1, const int &x2, const int &y2);
void ShowRecInfo()
};
|
cs |
6번째 라인이 바로 생성자인데요, 두 점의 정보를 직접 전달할 수 있게 정의하였습니다. 이 정보를 통해서 두 개의 Point 객체가 초기화되는 것이죠. 이어서 Rectangle 클래스의 생성자 정의를 보겠습니다.
1
|
Rectangle::Rectangle(const int &x1, const int &y1, const int &x2, const int &y2) : upLeft(x1, y1), lowRight(x2, y2) {}
|
cs |
여기에서 '멤버 이니셜라이저'는 :upLeft(x1, y1), lowRight(x2, y2) 이 부분이다. 다음과 같은 내용을 의미한다.
💡 객체 upLeft의 생성과정에서 x1과 y1을 인자로 전달받는 생성자를 호출하라.
💡 객체 lowRight의 생성과정에서 x2와 y2를 인자로 전달받는 생성자를 호출하라.
이렇듯 멤버 이니셜라이저는 멤버변수로 선언된 객체의 생성자 호출에 관여합니다. 그렇다면 전체 예제를 한 번 살펴봅시다.
⭐️ Point.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#ifndef __POINT_H__
#define __POINT_H__
class Point{
private:
int x;
int y;
public:
Point(const int &xpos, const int &ypos);
int GetX() const;
int GetY() const;
bool SetX(int xpos);
bool SetY(int ypos);
};
#endif
|
cs |
⭐️ Point.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
#include <iostream>
#include "Point.h"
using namespace std;
Point::Point(const int &xpos, const int &ypos) {
x = xpos;
y = ypos;
}
int Point::GetX() const {return x;}
int Point::GetY() const {return y;}
bool Point::SetX(int xpos) {
if(0>xpos || xpos > 100){
cout << "벗어난 범위의 값 전달" << endl;
return false;
}
x = xpos;
return true;
}
bool Point::SetY(int ypos) {
if(0>ypos || ypos > 100){
cout << "벗어난 범위의 값 전달"<<endl;
return false;
}
y = ypos;
return true;
}
|
cs |
⭐️ Rectangle.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#ifndef __RECTANGLE_H__
#define __RECTANGLE_H__
#include "Point.h"
class Rectangle{
private:
Point upLeft;
Point lowRight;
public:
Rectangle(const int &x1, const int &y1, const int &x2, const int &y2);
void ShowRecInfo() const;
};
#endif
|
cs |
⭐️ Rectangle.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <iostream>
#include "Rectangle.h"
using namespace std;
Rectangle::Rectangle(const int &x1, const int &y1, const int &x2, const int &y2) : upLeft(x1, y2), lowRight(x2, y2){
}
void Rectangle::ShowRecInfo() const {
cout << "좌 상단: " << '[' << upLeft.GetX() << ", ";
cout << upLeft.GetY()<<']'<<endl;
cout << "우 하단: " << '[' << lowRight.GetX()<<", ";
cout << lowRight.GetY()<<']'<<endl<<endl;
}
|
cs |
⭐️ RectangleConstructor.cpp
1
2
3
4
5
6
7
8
9
10
|
#include <iostream>
#include "Point.h"
#include "Rectangle.h"
using namespace std;
int main(void){
Rectangle rec(1, 1, 5, 5);
rec.ShowRecInfo();
return 0;
}
|
cs |
생성자가 선택적으로 존재한다고 생각할 수 있습니다. 하지만 생성자를 명시하지 않는다고 하더라도 생성자는 디폴트로 만들어집니다.
🌼 소멸자의 이해와 활용
소멸자는 다음과 같은 형태를 갖습니다.
💡 클래스의 이름 앞에 '~'가 붙은 형태의 이름을 갖습니다.
💡 반환형이 선언되어 있지 않으며, 실제로 반환하지 않는다.
💡 매개변수는 void 형으로 선언되어야 하기 때문에 오버로딩도, 디폴트 값 설정도 불가능하다.
예를 들어서 다음의 형태를 갖춘 것이 소멸자입니다.
1
|
~AAA(){ ... }
|
cs |
이와 관련해서 예제를 살펴봅시다.
⭐️ Destructor.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
#include <iostream>
#include <cstring>
using namespace std;
class Person{
private:
char* name;
int age;
public:
Person(char *myname, int myage){
int len = strlen(myname) + 1;
name = new char[len];
strcpy(name, myname);
age = myage;
}
void ShowPersonInfo() const{
cout << "이름: "<< name << endl;
cout << "나이: "<< age << endl;
}
~Person(){
delete []name;
cout << "called destructor!"<<endl;
}
};
int main(void){
Person man1("Lee dong woo", 29);
Person man2("Jang dong gun", 41);
man1.ShowPersonInfo();
man2.ShowPersonInfo();
return 0;
}
|
cs |
⭐️ Destructor.cpp 실행 결과
1
2
3
4
5
6
|
이름: Lee dong woo
나이: 29
이름: Jang dong gun
나이: 41
called destructor!
called destructor!
|
cs |
소멸자는 객체별로 적용되며, 불필요한 메모리 사용을 막아줍니다.
📜 출처
윤성우(2010). 윤성우 열혈 C++ 프로그래밍. 오렌지미디어.
'𝐏𝐑𝐎𝐆𝐑𝐀𝐌𝐌𝐈𝐍𝐆 > 𝐂++' 카테고리의 다른 글
C++ :: 복사 생성자(Copy Constructor) (0) | 2020.08.16 |
---|---|
C++ :: 클래스와 배열, this 포인터 (0) | 2020.08.15 |
C++ :: 캡슐화(Encapsulation) (0) | 2020.08.14 |
C++:: 정보은닉(Information Hiding) (0) | 2020.08.14 |
C++ :: C++에서의 파일분할 (0) | 2020.08.13 |