Notice
Recent Posts
Recent Comments
«   2024/12   »
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
Archives
Today
Total
관리 메뉴

SYDev

C++ Chapter 04-3 : 생성자(Constructor)와 소멸자(Destructor)(2) 본문

Programming Lang/C++

C++ Chapter 04-3 : 생성자(Constructor)와 소멸자(Destructor)(2)

시데브 2023. 7. 16. 16:13

멤버 이니셜라이저를 이용한 변수 및 const 상수(변수) 초기화

 '멤버 이니셜라이저'는 객체가 아닌 멤버의 초기화에도 사용할 수 있다. 이와 관련된 다음 코드를 살펴보자.

class SoSimple
{
pirvate:
    int num1;
    int num2;
public:
    SoSimple(int n1, int n2) : num1(n1) // 이니셜라이저(num1의 값을 n1로 초기화) 
    {
        num2 = n2;
    }
    . . . . .
}

 다음 이니셜라이저는 num1의 값을 n1로 초기화하라는 의미이며 선언과 동시에 초기화되는 것과 같은 유형의 바이너리 코드를 구성하여 다음 문장과 비유할 수 있다.

int num1 = n1; // 이니셜라이저

 반면, 생성자의 몸체에 있는 초기화 문장은 다음과 같이 선언과 초기화가 나눠져있다.

int num2;
num2 = n2;

 우리는 이런 이니셜라이저의 특징을 const 변수와 연결할 수 있다(const 변수는 선언과 초기화를 동시에 해야 하는 특징을 가짐). 그렇다면 이니셜라이저를 이용하여 const 멤버변수를 초기화 가능할 것이다.

 

 이와 관련하여 앞 게시글에서 보인 예제 FruitSaleSim2.cpp에서 정의한 FruitSeller 클래스의 멤버변수 APPLE_PRICE는 const로 선언이 가능하다. 해당 예제를 재구현해보자.

#include <iostream>
using namespace std;

class FruitSeller
{
    const int APPLE_PRICE; 
    int numOfApples;
    int myMoney;

public:
	FruitSeller(int price, int num, int money) 
        : APPLE_PRICE(price), numOfApples(num), myMoney(money) // 멤버 변수 이니셜라이저 (APPLE_PRICE는 const 변수)
    {
    }

    int SalesApples(int money) 
    {
        int num = money/1000; 
        numOfApples -= num; 
        myMoney += money; 
        return num; 
    }
    
    void ShowSalesResult() const
    {
        cout<<"남은 사과 : "<<numOfApples<<endl;
        cout<<"판매 수익 : "<<myMoney<<endl;
    }
};

class FruitBuyer
{
    int myMoney; 
    int numOfApples;  

public:
    FruitBuyer(int money)
        :myMoney(money), numOfApples(0) // 멤버 변수 이니셜라이저
    {
    }

    void BuyApples(FruitSeller &seller, int money) 
    {
        numOfApples += seller.SalesApples(money);
        myMoney -= money;
    }
    void ShowBuyResult() const
    {
        cout<<"현재 잔액 : "<<myMoney<<endl;
        cout<<"사과 개수 : "<<numOfApples<<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;
}

 

멤버 이니셜라이저를 이용한 참조자 멤버변수 초기화

 const 변수와 마찬가지로 '참조자'도 선언과 초기화가 동시에 이뤄져야 하는 조건을 가지고 있다. 그렇다면 이니셜라이저를 이용해서 참조자 또한 멤버변수로 선언할 수 있다.

 

 이러한 특성을 나타내는 다음 예제를 살펴보자.

#include <iostream>
using namespace std;

class AAA
{
public:
    AAA()
    {
        cout<<"empty object"<<endl;
    }

    void ShowYourName()
    {
        cout<<"I'm class AAA"<<endl;
    }
};

class BBB
{
private:
    AAA &ref;
    const int &num;
public:
    BBB(AAA &r, const int &n)
        : ref(r), num(n)
    {
        //empty constructor body
    }

    void ShowYourName()
    {
        ref.ShowYourName();
        cout<<"and"<<endl;
        cout<<"I ref num "<<num<<endl;
    }
};

int main(void)
{
    AAA obj1;
    BBB obj2(obj1, 20);
    obj2.ShowYourName();

    return 0;
}
empty object
I'm class AAA
and
I ref num 20

 

디폴트 생성자(Default Constructor)

 지난 게시글의 마지막에 언급했듯이 메모리 공간의 할당 이후에 생성자의 호출까지 완료되어야 '객체'이다. 다시 말해서, 객체가 되기 위해서는 반드시 하나의 생성자가 호출되어야 한다는 것이다. 따라서 따로 생성자를 정의하지 않는 클래스에는 C++ 컴파일러가 디폴트 생성자를 자동으로 삽입한다.

 

 따라서 다음과 같이 정의된 클래스는, 

class AAA
{
private:
    int num;
public:
    int GetNum { return num; }
};

 디폴트 생성자가 삽입되기 때문에 다음 클래스 정의와 동일하다.

class AAA
{
private:
    int num;
public:
    AAA(){ } // 디폴트 생성자
    int GetNum { return num; }
};

 결론적으로, 모든 객체는 한 번의 생성자 호출을 반드시 동반한다. 또한, new 연산자를 이용한 객체의 생성에도 이는 해당된다. 하지만 malloc 함수 호출시에는 AAA클래스의 크기정보만 바이트 단위로 전달되기 때문에 생성자가 호출되지 않는다. 따라서 객체를 동적으로 할당하는 경우에는 반드시 new 함수를 이용해야 한다. 

 

생성자 불일치

 매개변수가 void형으로 선언되는 디폴트 생성자는, 클래스에 생성자가 하나도 정의되지 않았을 때에만 자동으로 삽입된다. 

 

 다음 클래스가 있을 때,

class SoSimple
{
private:
    int num;
public:
    SoSimple(int n) : num(n) { }
}

 다음 형태로는 객체 생성이 가능하다.

SoSimple simObj1(10);
SoSimple * simPtr1 = new SoSimple(2);

 하지만, 다음 형태로의 객체 생성은 불가능하다.

SoSimple simObj2; // compile error
SoSimple * simPtr2 = new SoSimple; // compile error

 위의 형태로 객체 생성을 원한다면 다음 형태로 생성자를 추가해야 한다.

SoSimple() : num(0) { }

 

private 생성자

 앞선 내용에서 생성자는 모두 public 영역에 선언되었다. 객체의 생성이 클래스의 외부에서 진행되기 때문에 생성자는 public 영역에서 생성되어야 한다.

 

 그렇다면 private 영역에서 생성자가 선언되는 경우는 없을까? 객체가 클래스 내부에서 생성되는 경우에는 private 영역에서 생성자를 선언할 수 있다. 또한 이 경우에는 클래스 내부에서만 객체가 생성되어야 한다.

 

 이와 관련하여 아래 예제를 살펴보자.

#include <iostream>
using namespace std;

class AAA
{
private:
    int num;
public:
    AAA() : num(0) {}
    AAA& CreateInitObj(int n) const
    {
        AAA * ptr = new AAA(n); // private에 선언된 생성자 이용 ,동적할당과 동시에 변수 초기화
        return *ptr;
    }
    void ShowNum() const { cout<<num<<endl; }
private:
    AAA(int n) : num(n) {} // 위 함수의 정의에 이용된 private 영역의 생성자
};

int main(void)
{
    AAA base;
    base.ShowNum();

    AAA &obj1= base.CreateInitObj(3); // 참조자 obj1의 메모리 공간에 3 저장
    obj1.ShowNum();

    AAA &obj2= base.CreateInitObj(12); // 참조자 obj2의 메모리 공간에 12 저장
    obj2.ShowNum();

    delete &obj1;
    delete &obj2;

    return 0;
}
0
3
12

 

소멸자의 이해와 활용

 생성자는 객체 생성 시에 반드시 호출되는 것처럼, 소멸자객체가 소멸할 때 반드시 호출된다. 소멸자의 형태는 다음과 같다. 

  • 클래스의 이름 앞에 '~'가 붙은 형태의 이름
  • 반환형 선언 X, 실제로 반환도 X
  • 매개변수는 void형으로 선언 -> 오버로딩, 디폴트 값 설정 불가능

 예를 들어 AAA라는 클래스가 정의되면, 소멸자는 다음의 형태를 갖춘다.

~AAA() { . . . . }

 소멸자는 객체의 소멸 과정에서 자동으로 호출되고, 생성자와 마찬가지로 정의된 소멸자가 없다면 디폴트 소멸자가 자동으로 삽입된다.

 

 즉, 다음 클래스의 정의는 

Class AAA
{
	///empty Class
};

 다음의 클래스 정의와 일치한다.

class AAA
{
public:
    AAA() { }
    ~AAA() { }
};

 소멸자는 대개 생성자에서 할당한 리소스의 소멸에 사용된다. 예를 들어서 생성자에서 new 연산자를 이용하여 메모리 공간을 할당하면, 소멸자에서는 delete 연산자를 이용하여 이 메모리 공간을 소멸시킨다.

 

 이와 관련된 예제를 살펴보자.

#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;
}
이름 : Lee dong woo
나이 : 29
이름 : Jang dong gun
나이 : 41
called destructor!
called destructor!

 

+ 생성자가 아닌 멤버 함수에서 new를 이용한 동적할당이 이루어졌을 때에도 소멸자에서 delete하는 것이 맞을까? 물론 가능은 하겠지만 효율적이지 않다. 멤버 함수를 이용해서 얼마나 많은 메모리 공간이 할당될지 모르기 때문이다. 때문에 멤버함수에서 할당된 메모리 공간들은 main 함수에서 마지막에 delete해주는 것이 좋다.


참고자료

 

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

주의 사항! 이 글은 제가 직접 공부하는 중에 작성되고 있습니다. 따라서 제가 이해하는 그대로의 내용이 포함됩니다. 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다. 생성자 지금까

koey.tistory.com