본문 바로가기
c++

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

by goblin- 2023. 2. 25.

우리는 객체를 생성할 때 정보은닉을 목적으로 멤버변수들을 private으로 선언했다. 따라서 멤버변수를 초기화하기 위해서는 따로 함수를 정의하고 호출하였다. 하지만 '생성자'를 이용하면 객체도 생성과 동시에 초기화할 수 있다.

 

생성자를 이해하기 위해 다음 예를 살펴보겠다.

class SimpleClass
{
private:
	int num;
public:
	SimipleClass(int n)	//생성자(constructor)
    {
    	num=n;
    }
    int GetNum() const
    {
    	return num;
    }
};

위 코드를 살펴보면 SimpleClass함수는 클래스의 이름과 이름이 동일하고 반환형이 선언되어 있지 않으며, 실제로 반환하지 않는다.

이러한 유형을 '생성자(constructor)'라고 하며 객체 생성 시 한 번만 호출된다는 특징을 갖는다. 

 

생성자를 정의하지 않았을 때, 우리는 다음과 같은 방식을 객체를 생성하였다.

SimpleClass sc;
SimpleClass *ptr=new SipleClass;

하지만 생성자가 정의되어있을 경우, 객체생서과정에서 자동으로 호출되는 생성자에게 전달할 인자의 정보를 다음과 같이 추가해야 한다.

SimpleClass sc(20);
SimipleClass * ptr = new SimpleClass(30);

 

 

생성자는 함수의 일종이기 때문에 오버로딩이 가능하고 매개변수에 디폴트 값을 설정할 수 있다. 아래 코드를 살펴보자.

#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;
}
결과
0 0
100 0
100 200

위의 코드를 보면 생성자는 오버로딩이 가능하다는 것을 알 수 있다. 또한 매개변수의 디폴트 값까지 설정할 수 있다는 것을 보여준다.

하지만 위의 코드에서 SimpleClass(int n1, int n2)와 SimpleClass(int n1=0, int n2=0) 은 매개변수의 형태와 인자의 수가 같기 때문에 객체를 생성할 때 어떤 생성자를 호출할지 애매해진다. 따라서 오버로딩의 특성에 맞춰 인자의 개수나 매개변수의 형태를 다르게 해야 오류가 나지 않는다.

(SimpleClass() 이렇게 정의된 생성자를 이용하여 객체를 생성하기 위해서는 SimpleClass s1(); 과 같이 생성하면 에러가 발생한다 따라서 SimpleClass s1; 이런 식으로 생성해야 한다.)

 

디폴트 생성자(Default Constructor)

메모리 공간의 할당 이우헤 생성자의 호출까지 완료되어야 객체라 할 수 있다. 즉, 객체가 되기 위해서는 반드시 하나의 생성자가 호출되어야 한다. 그리고 이러한 기준에 예외를 두지 않기 위해서 생성자를 정의하지 않는 클래스에는 C++ 컴파일러에 의해서 디폴트 생성자라는 것이 자동으로 삽입된다. 그런데 디폴트 생성자는 인자를 받지 않으며,  내부적을 아무런 일도 하지 않는 생성자이다. 

 

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

예제 1번에는 생성자를 정의하지 않았지만 예제 2와 같이 기본적으로 디폴트 생성자가 자동으로 삽입된다 따라서 예제 1과 예제 2는 기능적으로 같은 코드이다.

 

하지만 여기서 주의해야 할 점은 생성자가 하나도 정의되어 있지 않을 때에만 디폴트 생성자가 삽입된다. 

예를 들어 SoSimple(int n) : num(n) {}과 같은 생성자가 선언이 되어있으면 디폴트 생성자가 삽입되지 않으므로 SoS example; 과 같은 객체 생성은 불가능하다. (SoSimple(int n) : num(n) {} 은 예시로 SoSimple example(10); 이런 식으로 선언해야 한다.)

 

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);
            return *ptr;
        }
        void ShowNum() const { cout<<num<<endl;}
    private:
        AAA(int n) : num(n) {}
};

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

    AAA &obj1=base.CreateInitObj(3);
    obj1.ShowNum();

    AAA &obj2=base.CreateInitObj(12);
    obj2.ShowNum();

    delete &obj1;
    delete &obj2;
    return 0;
}

결과

0
3
12

위 예제에서는 힙 영역에 생성된 객체를 참조의 형태로 반환하고 있다. 이는 힙에 할당된 메모리 공간은 변수로 간주하여, 참조자를 통한 참조가 가능하다는 것을 알 수 있다. 그리고 위 예제에서는 단순히 private으로 선언된 생성자를 통해서도 객체의 생성이 가능함을 보였다. private생성자는 특히 객체의 생성방법을 제한하고자 하는 경우에 매우 유용하게 사용이 된다.

 

소멸자의 이해와 활용

객체생성 시 반드시 호출되는 것이 생성자라면, 객체소멸 시 반드시 호출되는 것은 소멸자이다. 소멸자는 다음의 형태를 갖는다.

  • 클래스의 이름 앞에'~'가 붙은 형태의 이름을 갖는다.
  • 반환형이 선언되어 있지 않으며, 실제로 반환하지 않는다.
  • 매개변수는 void형으로 선언되어야 하기 때문에 오버로딩도, 디폴트 값 설정도 불가능하다.

소멸자는 ex)~AAA() {....} 이런 식으로 선언하고 객체소멸 과정에서 자동으로 호출된다. 프로그래머가 직접 소멸자를 정의하지 않으면, 디폴트 생성자와 마찬가지로 아무런 일도 하지 않는 디폴트 소멸자가 자동으로 삽입된다.

class AAA{

};
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 ondg gun",41);
    man1.ShowPersonInfo();
    man2.ShowPersonInfo();
    return 0;
}
결과
이름: Lee dong woo
나이: 29
이름: Jang ondg gun
나이: 41
called destructor!
called destructor!

위 예제를 보면 소멸자를 통해서 객체소멸과정에서 처리해야 할 일들을 자동으로 처리할 수 있다.