본문 바로가기
  • 쓸쓸한 개발자의 공부방
C++

[C++] 생성자, 소멸자, 위임 생성자, default 생성자

by 심찬 2021. 8. 19.

 

생성자 (constructor)와 소멸자 (destructor)

 

 

"생성자 (constructor)"

 

생성자를 사용하는 이유

 - 객체를 자동으로 초기화 하기 위해서

 

생성자 모양

 - 클래스 이름과 동일한 함수

 - 리턴 타입이 없다.

 - 인자는 있어도 되고 없어도 된다. 2개 이상 만들 수 있다.

 

객체를 생성하면

 - 객체의 크기 만큼 메모리를 할당

 - 생성자가 호출된다

 - 생성자가 없으면 객체를 만들 수 없다.

 

디폴트 생성자

 - 사용자가 생성자를 한 개도 만들지 않으면 디폴드 생성자를 컴파일러가 인자없는 생성자를 제공해 준다.

#include <iostream>
using namespace std;

class Point
{
    int x, y;
public:
    Point()             { x = 0; y = 0; cout << "1" << endl;}
    Point(int a, int b) { x = a; y = b; cout << "2" << endl;}
};

int main()
{
    Point p1;       // 1
    Point p2(1, 2); // 2
}

 

객체를 생성하는 다양한 방법

아래 코드에서 다양한 방식으로 객체를 생성하는 모습을 확인해 볼 수 있다. 

#include <iostream>
using namespace std;

class Point
{
    int x, y;
public:
    Point()             { x = 0; y = 0; cout << "1" << endl;}
    Point(int a, int b) { x = a; y = b; cout << "2" << endl;}
};

int main()
{
    cout << "== 1 =========================================" << endl;
    Point p1(1,2);     // 2
    Point p2{1,2};     // 2,  C++11 직접 초기화 방식 가능
    Point p3 = {1,2};  // 2,  C++11 복사 초기화 방식 가능
    
    cout << "== 2 =========================================" << endl;
    
    Point p4;       // 1
    Point p5();     // 객체 생성 아님. 함수 선언문
    Point p6{};     // 1
    Point p7 = {};  // 1
    
    cout << "== 3 =========================================" << endl;

    Point p8[3];    // 1번 생성자 3번 호출
    
    Point p9[3] = {Point(1,1)}; // 2번 생성자 1회 호출
                                // 1번 생성자 2회 호출됨.
    
    Point p10[3] = { {1,1}, {2,2} }; // C++11 
    
    cout << "== 4 =========================================" << endl;
   
    Point* p11; // 객체 생성 아님.
    
    p11 = static_cast<Point*>( malloc(sizeof(Point)));
    free(p11);
    
    p11 = new Point;  // 1번 생성자 호출
    delete p11;
    
    p11 = new Point(); // 1번.. 
    delete p11;
    
    p11 = new Point(1,2); // 2번 
    delete p11;

}

 

 

생성자와 소멸자 호출 순서

 

객체가 생성되면

 - 멤버의 생성자가 먼저 호출되고

 - 자신의 생성자가 호출된다.

 

객체가 파괴되면

 - 자신의 소멸자가 먼저 호출되고

 - 멤버의 소멸자가 호출된다.

 

 

#include <iostream>

class Point
{
    int x, y;
public:
    Point()  { std::cout << "Point()"  << std::endl;}
    ~Point() { std::cout << "~Point()" << std::endl;}
};

class Rect
{
    Point p1;
    Point p2;
public:
    Rect()  { std::cout << "Rect()"  << std::endl;}
    ~Rect() { std::cout << "~Rect()" << std::endl;}
};

int main()
{
    Rect r;
}

아래 출력을 통해 생성자/파괴자 호출 순서를 확인해 볼 수 있다.

 

 

위임 생성자 (delegate constructor)

 

Point() 생성자가 호출되면 Point(0,0) 를 호출한다.

기존에 만들어둔 생성자를 재활용할 수 있다.

class Point
{
    int x, y;
public:
    Point() : Point(0, 0)
    {
    //    x = 0;
    //    y = 0;
    }
    Point(int a, int b) 
    {
        x = a;
        y = b;
    }
};

int main()
{
    Point p;

}

 

선언부와 구현부가 구분되어 있을 때, 구현부에 Point(0,0) 를 넣어 주어야 한다.

 

// =========== Point.h =============
class Point
{
    int x, y;
public:
    Point();
    Point(int a, int b);
};

 

// =========== Point.cpp =============

#include "Point.h"

Point::Point() : Point(0,0)
{
}

Point::Point( int a, int b)
{
    x = a;
    y = b;
}

 

인자가 3개인 생성자가 인자가 2개인 생성자를 호출하도록 위임 생성자 코딩을 사용했을 때 :

#include <iostream>
using namespace std;

class Point
{
    int x, y;
public:
    Point() : Point(0, 0) { }

    Point(int a, int b) {
        x = a;
        y = b;
        cout << "Point (a,b) called with "<< a << "," << b <<endl;
    }
    
    Point(int a, int b, int c) : Point (a, b) {
        x = a + 10;
        y = b + 10;
        cout << "Point (a,b,c) called with "<< a << "," << b << "," << c<<endl;
    }
    void print() {
        cout << "x : " << x <<endl;
        cout << "y : " << y <<endl;
    }
};

int main()
{
    Point p;
    p.print();
    
    Point p2(5,6);
    p2.print();
    
    Point p3 (1,2,3);
    p3.print();
}

Point p3(1,2,3); 이 호출되었을 때 위임 생성자에 의해 먼저 Point(a,b) 가 실행되고, 그 다음에 Point(a,b,c) 생성자의 내용이 수행되는 것을 확인할 수 있다.

생성자가 추가될 때 위임 생성자를 활용해 중복 코딩을 피하는데 활용할 수 있다.

 

 

 

디폴트 생성자 (default constructor)

 

Point() = default;

 - 컴파일러에게 디폴트 생성자를 만들어 달라는 문법

 - 클래스 선언부에게 표기하면 되고 구현부는 만들지 않아도 된다.

class Point
{
    int x;
    int y;
public:
    
    //Point() = delete;   // 이런 코딩도 가능 : 디폴드 생성자 삭제 (그냥 참조로만...)
    Point() = default;    // 이렇게 default 생성자 선언할 수 있다.
    
    //Point() {}          // 이렇게 기본 생성자를 직접 만들 수 있는데 구현부에 구현이 필요함!
                          // 그래서 default 명시적으로 생성자 선언을 추천
    
    Point(int a, int b) {}
};

int main()
{
    Point p1;
    Point p2(1,2); // ok
}

 

선언부와 구현부가 구분되어 있을 때, defalut constructor는 선언부에만 표기해주면 된다.

// ===============Point.h ============
class Point
{
    int x;
    int y;
public:
    Point() = default;
    Point(int a, int b);
};

 

// ============Point.cpp===============

#include "Point.h"

Point::Point(int a, int b)
{
    x = a;
    y = b;
}

 

 

소멸자 (destructor)

 

~클래스이름()

리턴 타입 표기하지 않는다

인자를 가질 수 없다

한개만 만들 수 있다.

 

객체를 생성하면 생성자가 호출되고

객체가 파괴되면 소멸자가 호출된다.

 

소멸자를 만들지 않으면 컴파일러가 소멸자를 제공해 준다.

 

생성자에서 자원을 할당한 경우, 소멸자에서 자원을 해지 해야 한다.

자원 해지 등이 필요없는 경우 소멸자 만들 필요는 없다.

#include <iostream>

class Point
{
    int x, y;
public:
    Point()  { std::cout << "Point()"  << std::endl;}
    //~Point() { std::cout << "~Point()" << std::endl;}  
          // 메모리 할당 같은 코드가 없으니 굳이 소멸자를 만들지 않아도 된다.
};

int main()
{
    Point pt;
}

 

소멸자 사용 예시

 

C++를 사용해서 생성자에서 자원을 할당하고 소멸자에서 자원을 해지하면 자동으로 자원이 관리 된다.

 

C언어에서 f를 만들어 두고 반드시 사용자가 직접 반납 해야 한다. 변수에 직접 접근이 되어 문제 발생 소지가 많다.

#include <stdio.h>

int main()
{
    FILE* f = fopen("a.txt", "wt");

    f = 0; // 사용자가 실수로 값을 넣어버리면 아래 코드들이 잘못된 행동을 하게 된다.
           // 변수에 직접 접근하기에 이런 문제가 발생할 수 있다.
    
    fputs("hello", f);

    fclose(f);    
    // 해당 자원 반납 코드가 없어도 자원 반납이 되지만
    // 루프같은 코드가 수행 중일 때 문제 발생 가능성이 높다.
}

 

파일 관리하는 코드를 클래스로 만든 예시

 

C++에서 널리 사용되는 코딩 스타일 :

 - 생성자에서 자원을 할당한다.

 - 소멸자에서 자원을 반납한다.

 - 자원의 번호를 담은 멤버 변수는 private영역에 놓고 외부에서 직접 접근 할 수 없게 만든다.

#include <iostream>
#include <cstdio>
#include <string>

class File 
{
    FILE* file = 0;
public:
    File( std::string filename, std::string mode)
    {
        file = fopen(filename.c_str(), mode.c_str() );
    }
    ~File()
    {
        fclose(file);
    }
    // RW function
    void puts( std::string s)
    {
        fputs( s.c_str(), file);
    }
};

int main()
{
    // 새로운 객체 지향 코드
    File f("a.txt", "wt");
    f.puts("hello");
}

a.txt에 hello가 적힌 파일이 생성되었다.

 

 

 

댓글