C++(cpp)

[cpp] 소멸자, 복사 생성자

Jueun Park 2021. 6. 24. 12:38

소멸자

생성자의 반대격, 객체가 사라질 때 호출되는 함수(자동으로)이다. 객체 내에 new로 동적 생성한 변수 등이 있을때 유용함.

  • 생성자와의 공통점: 디폴트 생성자가 있음, 그러나 아무 행동도 하지 않음.
  • 생성자와의 차이점: 오버로딩 불가능
  • 형식: ~(클래스의 이름)
  • 사용예시
#include <iostream>

class Example{
    private:
    char* _name;

    public:
    // 생성자
    Example(){
        _name = NULL;
    }
    Example(char const* name){
        _name = new char[strlen(name) + 1];
        strcpy(_name, name);
    }

    // 소멸자
    ~Example(){
        if (_name) { 
            std::cout << _name << " : name deleted\n" << std::endl;
            delete[] _name; 
        }
        else {
            std::cout << "noname" << " : has noname\n" << std::endl;
        }
    }
};

int
    main()
{
    Example noname;
    Example cadet("cadet");
    Example ensign("ensign");
    Example commander("commander");

    return (0);
}

결과창:

commander : name deleted

ensign : name deleted

cadet : name deleted

noname : has noname

 


 

복사 생성자

해당 클래스의 객체를 인자로 넣으면, 그대로 복사해주는 생성자. 같은 상수를 가진 객체를 만들기 쉽다.

 [ 예제1 ] cpp에서 제공하는 복사 생성자를 이용한 경우(디폴트 복사 생성자)

#include <iostream>

class Example {
    private:
    int _x;
    int _y;

    public:
    Example(int x, int y);
    void print();
};

Example::Example(int x, int y) : _x(x), _y(y) {
    std::cout << "생성자 호출" << std::endl;
}

void Example::print() {
    std::cout << "_x: " << _x << std::endl;
    std::cout << "_y: " << _y << std::endl;
}

int
    main()
{
    Example example(1, 3);
    example.print();
    Example example2(example);
    example2.print();
    Example example3 = example2;
    example2.print();
}

결과창:

생성자 호출
_x: 1
_y: 3
_x: 1
_y: 3
_x: 1
_y: 3

 [ 예제2 ] 내가 만든 복사 생성자를 이용

#include <iostream>

class Example {
    private:
    int _x;
    int _y;

    public:
    Example(int x, int y);
    Example(Example const& copy);
    void print();
};

Example::Example(int x, int y) : _x(x), _y(y) {
    std::cout << "생성자 호출" << std::endl;
}

Example::Example(Example const& copy){
    std::cout << "복사생성자 호출" << std::endl;
    _x = copy._x;
    _y = copy._y;
}

void Example::print() {
    std::cout << "_x: " << _x << std::endl;
    std::cout << "_y: " << _y << std::endl;
}

int
    main()
{
    Example example(1, 3);
    example.print();
    Example example2(example);
    example2.print();
    Example example3 = example2;
    example2.print();
}

결과창:

생성자 호출
_x: 1
_y: 3
복사생성자 호출
_x: 1
_y: 3
복사생성자 호출
_x: 1
_y: 3

 

복사 생성자의 한계

디폴트 복사 생성자를 이용하여 복사 생성 해주는 아래와 같은 코드를 짰다.

#include <iostream>
#include <string.h>

class Example {
    private:
    int _x;
    int _y;
    char* _name;
 
    public:
    Example(int x, int y, char const* name);
    void print();
};

Example::Example(int x, int y, char const* name) : _x(x), _y(y) {
    _name = new char[strlen(name + 1)];
    strcpy(_name, name);
    std::cout << "생성자 호출" << std::endl;
}

void Example::print() {
    std::cout << "name: " << _name << std::endl;
    std::cout << "name-address: " << &_name << std::endl;
    std::cout << "_x: " << _x << std::endl;
    std::cout << "_y: " << _y << std::endl;
}

int
    main()
{
    Example example(1, 3, "example");
    example.print();
    Example example2(example);
    example.print();

    Example example3(1, 3, "example");
    example2.print();
    Example example4(example3);
    example2.print();
}

 

각 객체의 name의 주소를 뽑아보면

결과창:

생성자 호출
name: example
name-address: 0x7ffee52a0808
_x: 1
_y: 3
name: example
name-address: 0x7ffee52a0808
_x: 1
_y: 3
생성자 호출
name: example
name-address: 0x7ffee52a07e8
_x: 1
_y: 3
name: example
name-address: 0x7ffee52a07e8
_x: 1
_y: 3

첫번째 객체는 생성된 것이고, 두번째 객체는 복사된 객체

세번째 객체는 생성된 것이고, 네번재 객체는 복사된 객체

첫번째와 두번째 객체의 _name 멤버변수는 같은 주소를 가리키고 있고

세번째와 네번째 객체의 _name 멤버변수는 같은 주소를 가리키고 있다.

만약에 아래처럼 소멸자에 _name을 delete해주는 코드를 추가한다면

#include <iostream>
#include <string.h>

class Example {
    private:
    int _x;
    int _y;
    char* _name;
 
    public:
    Example(int x, int y, char const* name);
    void print();
    ~Example();
};

Example::Example(int x, int y, char const* name) : _x(x), _y(y) {
    _name = new char[strlen(name + 1)];
    strcpy(_name, name);
    std::cout << "생성자 호출" << std::endl;
}

Example::~Example(){
    delete[] _name;
}

void Example::print() {
    std::cout << "name: " << _name << std::endl;
    std::cout << "name-address: " << &_name << std::endl;
    std::cout << "_x: " << _x << std::endl;
    std::cout << "_y: " << _y << std::endl;
}

int
    main()
{
    Example example(1, 3, "example");
    example.print();
    Example example2(example);
    example.print();

    Example example3(1, 3, "example");
    example2.print();
    Example example4(example3);
    example2.print();
}
생성자 호출
name: example
name-address: 0x7ffee8ab8808
_x: 1
_y: 3
name: example
name-address: 0x7ffee8ab8808
_x: 1
_y: 3
생성자 호출
name: example
name-address: 0x7ffee8ab87e8
_x: 1
_y: 3
name: example
name-address: 0x7ffee8ab87e8
_x: 1
_y: 3
소멸자 호출
소멸자 호출
a.out(82457,0x109f95dc0) malloc: *** error for object 0x7fd5ef4058b0: pointer being freed was not allocated
a.out(82457,0x109f95dc0) malloc: *** set a breakpoint in malloc_error_break to debug
[1]    82457 abort      ./a.out

위와 같은 에러가 난다.

(네번째 객체의 멤버변수인 _name을 free하고, 세번째 객체의 멤버변수인 _name을 free하려고 할때에 에러가 나게되기 때문에 소멸자 호출이라는 메세지가 두번만 표시됨)

디폴트 복사 생성자를 사용하게 될 경우를 얕은 복사라고 부름. char 배열 등 깊은 복사(deep copy)가 필요한 것들은 디폴트 복사 생성자를 이용하지 말고 따로 동적 할당까지 해서 복사해주는 함수를 만들어서 사용해야함.

deep copy 예제

#include <iostream>
#include <string.h>

class Example {
    private:
    int _x;
    int _y;
    char* _name;
 
    public:
    Example(int x, int y, char const* name);
    Example(Example const& copy);
    void print();
    ~Example();
};

Example::Example(int x, int y, char const* name) : _x(x), _y(y) {
    _name = new char[strlen(name + 1)];
    strcpy(_name, name);
    std::cout << "생성자 호출" << std::endl;
}

Example::Example(Example const& copy){
    std::cout << "(Deep copy) 복사생성자 호출" << std::endl;
    _name = new char[strlen(copy._name + 1)];
    strcpy(_name, copy._name);
    _x = copy._x;
    _y = copy._y;
}

Example::~Example(){
    std::cout << "소멸자 호출" << std::endl;
    delete[] _name;
}

void Example::print() {
    std::cout << "name: " << _name << std::endl;
    std::cout << "name-address: " << &_name << std::endl;
    std::cout << "_x: " << _x << std::endl;
    std::cout << "_y: " << _y << std::endl;
}

int
    main()
{
    Example example(1, 3, "example");
    example.print();
    Example example2(example);
    example.print();

    Example example3(1, 3, "example");
    example2.print();
    Example example4(example3);
    example2.print();
}