도순씨의 코딩일지

C++ :: 생성자(Constructor)와 소멸자(Destructor) 본문

𝐏𝐑𝐎𝐆𝐑𝐀𝐌𝐌𝐈𝐍𝐆/𝐂++

C++ :: 생성자(Constructor)와 소멸자(Destructor)

도순씨 2020. 8. 15. 00:00

매개변수들을 초기하기 위해서 함수를 정의해주었는데, 매우 번거로운 일이었습니다. 이러한 불편함을 막기 위해서 생성자가 만들어졌습니다. 생성자를 이용하여 객체도 생성과 동시에 초기화할 수 있습니다.

 

🌼 생성자의 이해

생성자의 이해를 위해서 간단한 클래스 하나를 살펴보도록 합시다.

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(100200);
    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 = 0int 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(2030);
    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(1000200);
    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(1155);
    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++ 프로그래밍. 오렌지미디어.

Comments