[디딤돌 C++] 16. 생성자, 소멸자

이번에는 개체를 생성할 때 수행하는 생성자와 소멸할 때 수행하는 소멸자를 알아볼게요.

생성자는 개체를 생성할 때 수행할 기능을 정의하는 특별한 메서드입니다. 생성자는 반환 형식을 개발자가 정할 수 없으며 메서드 이름을 형식 이름과 같게 정의합니다.

그리고 소멸자는 개체를 소멸할 때 수행할 기능을 정의하는 특별한 메서드예요. 소멸자도 반환 형식을 개발자가 정할 수 없으며 메서드 이름은 ~형식 이름이예요.

#pragma once
//Student.h
class Student
{
public:
    Student(void);//생성자
    ~Student(void);//소멸자
};

C++ 언어에세 생성자는 개체가 만들어지는 시점에 동작합니다. 예를 들어 Student stu; 처럼 클래스 형식 변수 선언문에 의해 stu 개체가 만들어질 때도 동작하고 Student *stu = new Student(); 처럼 new 연산자를 사용하여 동적으로 개체를 생성할 때도 동작합니다.

소멸자는 개체의 메모리를 해제할 때 동작합니다. Student stu; 처럼 선언한 개체는 선언한 지역이 끝날 때 메모리를 해제하기 전에 소멸자를 먼저 수행합니다. Student *stu = new Student(); 처럼 동적으로 생성한 개체는 delete stu; 처럼 delete 연산으로 메모리 해제를 요청하면 소멸자를 먼저 수행하고 메모리를 해제합니다.

//Student.h
#pragma once
class Student
{
public:
    Student(void);//생성자
    ~Student(void);//소멸자
};
//Student.cpp
#include "Student.h"
#include <iostream>
using namespace std;

Student::Student(void)
{
    cout<<"학생 개체 생성자"<<endl;
}

Student::~Student(void)
{
    cout<<"학생 개체 소멸자"<<endl;
}

//Program.cpp
#include "Student.h"
#include <iostream>
using namespace std;
int main()
{
    Student stu1;
    cout<<"Test1"<<endl;
    Student *stu2 = new Student(); //동적으로 개체 생성
    cout<<"Test2"<<endl;
    delete stu2; //동적으로 생성한 개체 소멸
    cout<<"Test3"<<endl;
    return 0;
}

▷ 실행 결과

학생 개체 생성자
Test1
학생 개체 생성자
Test2
학생 개체 소멸자
Test3
학생 개체 소멸자
  1. 디폴트 기본 생성자, 디폴트 소멸자

클래스를 정의할 때 생성자와 소멸자를 정의하지 않으면 컴파일 할 때 접근 지정이 public인 디폴트 기본 생성자와 소멸자를 만들어 줍니다.

디폴트 기본 생성자와 소멸자가 실질적으로 수행하는 작업은 없지만 형식 외부에서 개체를 생성하거나 소멸할 수 있게 가시성을 제공하는 역할을 합니다.

DemoA 클래스에 생성자와 소멸자를 정의하지 않고 개체를 동적 생성 및 소멸을 요청하는 것은 가능합니다. 개발자가 정의하지 않으면 컴파일러가 접근 지정이 public인 디폴트 기본 생성자와 소멸자를 만들어 주기 때문이죠.

하지만 DemoB 클래스에 생성자와 소멸자를 정의하고 접근 지정자를 private으로 설정하면 디폴트 기본 생성자와 소멸자는 만들어 주지 않습니다. 이 때는 개발자가 정의한 생성자와 소멸자의 접근 지정이 private 이므로 형식 외부에서 개체를 생성할 수 없습니다.

다음은 이를 테스트하는 예제 코드예요.

#include <iostream>
using namespace std;

//생성자와 소멸자를 개발자가 정의하지 않음
//   컴파일러가 접근 지정이 public인 디폴트 기본 생성자와 소멸자를 만들어 줌
class DemoA 
{
};

class DemoB
{
    //테스트를 위해 의도적으로 생성자와 소멸자의 접근 지정을 private으로 설정
private:
    DemoB(void);
    ~DemoB(void);
};

int main()
{
    DemoA *da = new DemoA();
    delete da;
    DemoB *db = new DemoB();
    delete db;
    return 0;
}

다음은 이를 컴파일했을 때 발생하는 오류 화면을 캡쳐한 것입니다.

private 멤버 접근 오류
  1. 생성자 중복 정의

C++에서는 개체를 생성할 때 수행할 생성자를 중복 정의할 수 있습니다. 물론 중복 정의 문법에 따라 입력 매개 변수 리스트가 달라야겠죠.

참고로 소멸자는 중복 정의할 수 없어요.

class Student
{
    int num;
    string name;
public:
    //생성자 중복 정의
    Student(void);
    Student(int num);
    Student(int num,string name);
    void View();
};

사용하는 코드에서는 생성할 때 전달하는 인자에 따라 적절한 생성자를 호출합니다.

Student *stu1 = new Student();
Student *stu2 = new Student(3);
Student *stu3 = new Student(3,"홍길동");

다음은 생성자 중복 정의한 예제 코드입니다.

//Student.h
#pragma once
#include <iostream>
#include <string>
using namespace std;
class Student
{
    int num;
    string name;
public:
    //생성자 중복 정의
    Student(void);
    Student(int num);
    Student(int num,string name);
    void View();
};
//Student.cpp
#include "Student.h"

Student::Student(void)
{
    num = 0;
    name = "";
}
Student::Student(int _num)
{
    num = _num;
    name = "";
}
Student::Student(int _num,string _name)
{
    num = _num;
    name = _name;
}
void Student::View()
{
    if(num)
    {
        cout<<"번호:"<<num;
    }
    else
    {
        cout<<"번호:N/A";
    }
    if(name != "")
    {
        cout<<" 이름:"<<name<<endl;        
    }
    else
    {
        cout<<" 이름:NA"<<endl;
    }
}
//생성자 중복 정의
//Program.cpp
#include "Student.h"
int main()
{
    Student *stu1 = new Student(); 
    Student *stu2 = new Student(3);    
    Student *stu3 = new Student(3,"홍길동");
    
    stu1->View();
    stu2->View();
    stu3->View();
    
    delete stu1;
    delete stu2;
    delete stu3;
    
    return 0;
}

▷ 실행 결과

번호:N/A 이름:N/A
번호:3 이름:N/A
번호:3 이름:홍길동

3. 복사 생성자

생성자 중에 입력 인자로 같은 형식의 개체를 인자로 받는 생성자를 복사 생성자라고 부릅니다. 개발자가 복사 생성자를 정의하지 않으면 컴파일러가 디폴트 복사 생성자를 만들어요.

디폴트 복사 생성자는 입력 인자로 받은 개체의 메모리를 생성하는 개체의 메모리에 그대로 복사합니다. 이와 같이 개체의 메모리만 복사하는 것을 얕은 복사라고 말합니다.

DemoClass dc1(3);
DemoClass dc2(dc1); //복사 생성자에 의해 dc1의 메모리를 dc2 메모리에 복사

다음은 디폴트 복사 생성자에 동작을 확인하는 코드입니다.

//Program.cpp
#include <iostream>
using namespace std;

class DemoClass
{
    int num;
public:
    DemoClass(int _num)
    {
        num = _num;
    }
    void View()
    {
        cout<<"번호:"<<num<<endl;
    }
    
};

int main()
{
    DemoClass dc1(3);
    DemoClass dc2(dc1); //복사 생성자에 의해 dc1의 메모리를 dc2 메모리에 복사

    dc1.View();
    dc2.View();
    return 0;
}

▷ 실행 결과

번호: 3
번호: 3

그런데 형식 내부에서 동적으로 생성한 개체를 갖고 있을 때는 개발자가 복사 생성자를 정의해야 할 필요가 있습니다.

정수를 보관하는 동적 배열을 예로 들게요.

동적 배열 클래스 다이어그램

동적 배열 개체를 생성할 때 보관할 원소의 최대 개수를 입력받아 저장소를 동적으로 생성하게 합시다.

DCArray(int _capa)
{
    bcapacity = _capa;
    base = new int[bcapacity]; //bcapacity개수의 int 형식을 동적으로 생성
    usage = 0;
}

개체 내부에서 저장소를 동적 생성하므로 소멸자를 정의하여 이를 소멸하는 부분을 구현해야겠죠.

~DCArray()
{
    delete[] base;//동저으로 생성한 저장소 소멸(new []로 생성한 것은 delete[]로 소멸)
}

그리고 순자 보관하는 PushBack 메서드를 제공합니다.

void PushBack(int data)
{
    if(usage<bcapacity)//꽉 차지 않았을 때
    {
        base[usage] = data;
        usage++;//보관 개수 1 증가
    }
}

보관한 데이터 목록을 출력하는 메서드도 제공합시다.

void List()
{
    for(int i = 0; i<usage;i++)
    {
        cout<<base[i]<<" ";
    }
    cout<<endl;
}

이처럼 동적 배열 클래스를 만들었을 때 복사 생성하면 어떻게 동작하는지 알아볼게요.

DCArray dcarr1(10);//저장소의 크기가 10인 동적 배열 선언
dcarr1.PushBack(4);//순차 보관
DCArray dcarr2(dcarr1);//복사 생성으로 동적 배열 선언
dcarr1.List();
dcarr2.List();
dcarr1.PushBack(9);
dcarr2.PushBack(6);
dcarr1.List();
dcarr2.List();
pushback 메서드 전 후

복사 생성하면 단순히 dcarr1의 메모리를 dcarr2의 메모리에 복사하는 작업만 하기 때문에 내부 저장소를 별도로 생성하지 않고 같은 저장소를 가리킵니다.

그리고 dcarr1으로 9를 보관하면 저장소의 인덱스 1에 9를 보관하고 보관 개수인 usage를 1 증가하여 2로 변하겠죠. 그리고 dcarr2로 6을 보관하면 dcarr2의 usage멤버는 1인 상태이므로 저장소의 인덱스 1에 6을 보관하고 보관 개수인 usage를 1 증가하여 2로 변합니다.

이처럼 두 개의 배열 개체가 하나의 저장소를 사용하여 논리적 버그가 발생합니다. 이 외에도 소멸자에서 저장소를 해제하는 과정에서 한 쪽은 이미 해제한 저장소를 해제 요청하여 런타임 오류가 발생합니다.

다음은 앞에서 설명한 내용을 작성한 코드입니다.

//복사 생성자를 개발자가 정의할 필요하지만 정의하지 않았을 때
//Program.cpp

#include <iostream>
using namespace std;

//개발자가 복사 생성자를 정의하지 않음 - 디폴트 복사 생성자가 만들어짐
class DCArray
{
    int *base;
    int bcapacity;
    int usage;
public:
    DCArray(int _capa)
    {
        bcapacity = _capa;
        base = new int[bcapacity]; //bcapacity개수의 int 형식을 동적으로 생성
        usage = 0;
    }
    ~DCArray()
    {
        delete[] base;//동저으로 생성한 저장소 소멸(new []로 생성한 것은 delete[]로 소멸)
    }
    void PushBack(int data)
    {
        if(usage<bcapacity)//꽉 차지 않았을 때
        {
            base[usage] = data;
            usage++;//보관 개수 1 증가
        }
    }
    void List()
    {
        for(int i = 0; i<usage;i++)
        {
            cout<<base[i]<<" ";
        }
        cout<<endl;
    }
};

int main()
{
    DCArray dcarr1(10);//저장소의 크기가 10인 동적 배열 선언
    dcarr1.PushBack(4);//순차 보관
    DCArray dcarr2(dcarr1);//복사 생성으로 동적 배열 선언    
    dcarr1.List();
    dcarr2.List();
    dcarr1.PushBack(9);
    dcarr2.PushBack(6);
    dcarr1.List();
    dcarr2.List();
    return 0;
}

만약 개체 내부에서 동적으로 생성한 다른 개체를 멤버로 갖고 있다면 개발자가 복사 생성자를 중복 정의하세요. 개체의 메모리를 복사하는 얕은 복사만 할 것이 아니라 내부에 개체도 복사하여 갖게 하세요. 이와 같은 복사를 깊은 복사라고 부릅니다.

다음은 앞의 코드를 깊은 복사를 수행하는 복사 생성자를 정의한 예제 코드입니다.

//복사 생성자를 개발자가 정의하였을 때
//Program.cpp
#include <iostream>
using namespace std;

//개발자가 복사 생성자를 정의
class DCArray
{
    int *base;
    int bcapacity;
    int usage;
public:
    DCArray(int _capa)
    {
        bcapacity = _capa;
        base = new int[bcapacity]; //bcapacity개수의 int 형식을 동적으로 생성
        usage = 0;
    }

    DCArray(const DCArray &src)
    {
        bcapacity = src.bcapacity;
        base = new int[bcapacity];
        usage = src.usage;
        for(int i = 0; i<usage;i++)
        {
            base[i] = src.base[i];
        }
    }
    ~DCArray()
    {
        delete[] base;//동저으로 생성한 저장소 소멸(new []로 생성한 것은 delete[]로 소멸)
    } 
    void PushBack(int data)
    {
        if(usage<bcapacity)//꽉 차지 않았을 때
        {
            base[usage] = data;
            usage++;//보관 개수 1 증가
        }
    }
    void List()
    {
        for(int i = 0; i<usage;i++)
        {
            cout<<base[i]<<" ";
        }
        cout<<endl;
    }
};
int main()
{
    DCArray dcarr1(10);//저장소의 크기가 10인 동적 배열 선언
    dcarr1.PushBack(4);//순차 보관
    DCArray dcarr2(dcarr1);//복사 생성으로 동적 배열 선언    
    dcarr1.List();
    dcarr2.List();
    dcarr1.PushBack(9);
    dcarr2.PushBack(6);
    dcarr1.List();
    dcarr2.List();
    return 0;
}
복사 생성 후 pushback