C++/C++ 기초

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

Song 컴퓨터공학 2023. 5. 5. 15:09

포스팅의 목차 / 링크

1. 생성자 (Constructor)

2. 디폴트 생성자 (Default Constructr)

3. 복사 생성자 (Copy Constructor)

4. 소멸자 (Destructor)

5. 생성자/소멸자 실행 순서

 


클래스를 통해 객체를 생성하면, 해당 객체는 그 즉시 메모리에 생성됩니다. 그러나 이 객체는 모든 멤버 변수를 초기화 하기 전에는 사용할 수 없습니다. 객체의 멤버 변수는 사용자나 프로그램이 일반적인 초기화 방식으로 초기화 할 수 없는데요, 그 이유는 객체의 멤버 중 private 멤버가 있는 경우 이런 멤버에 직접 접근할 수 없기 때문입니다.

 

따라서 private 멤버에 접근할 수 있는 초기화 만을 위한 public 함수가 필요한데, 이런 초기화 함수는 객체가 생성된 후부터 사용되기 전까지 반드시 멤버의 초기화를 위해 호출되어야 합니다. 이 역할을 하는 것이 생성자(Constructor) 함수입니다.

 

 

생성자(Constructor)

 

C++ 은 객체의 생성과 동시에 멤버 변수를 초기화해주는 생성자(constructor) 라는 멤버 함수를 제공합니다. C++ 에서 클래스 생성자의 이름은 해당 클래스의 이름과 같습니다. 즉 circle 클래스의 생성자는 circle() 입니다. 이러한 생성자는 아래와 같은 특성을 가집니다.

 

1. 생성자는 초기화를 위한 데이터를 인수로 전달 받을 수 있다.

2. 생성자는 반환값이 없지만, void 형으로 선언하지 않는다.

3. 객체를 초기화하는 방법이 여러 개 존재할 경우 오버로딩 규칙에 따라 여러 개의 생성자를 가질 수 있다.

4. 객체 생성시에 딱 한 번 호출된다.

5. 생성자도 디폴트 값을 설정할 수 있다.


클래스는 객체가 생성될 때 자동으로 실행되는 생성자(constructor) 라는 특별한 멤버 함수를 통해 객체를 초기화 한다. 한 클래스에 여러 개의 생성자를 둘 수 있으나 이 중 하나만 실행된다.

  • 생성자의 목적은 객체가 생성될 때 필요한 초기 작업을 위함이다.
    • 멤버 변수의 값을 특정 값으로 설정하거나 메모리를 동적 할당 받거나 파일을 열거나 네트워크를 연결하는 등 객체를 사용하기 전에 필요한 조치를 할 수 있도록 하기 위함
  • 생성자 함수는 각 객체마다 객체가 생성되는 시점에 오직 한번만 실행된다.
  • 생성자 함수의 이름은 클래스 이름과 동일하게 작성되어야 한다.
  • 생성자 함수의 원형에 리턴 타입을 선언하지 않는다.
    • 따라서 생성자 함수는 함수 실행을 종료하기 위해 return 문을 사용할 수는 있지만 어떤 값도 리턴하면 안된다. 0이라도 return 하면 컴파일 오류
  • 생성자는 중복 가능하다.
    • 한 클래스에 여러 개를 만들 수 있다. 물론 매개변수 개수나 타입이 서로 다르게 선언되어야 한다. (함수 중복)

아래는 다양한 생성자의 예시 코드입니다. 오버로딩 규칙에 따라 여러 개의 생성자를 가질 수도 있으며, 객체를 생성할 때 호출 됩니다. 또한 반환값이 없는 것을 확인할 수 있지만 앞에 따로 void 형은 명시되지 않습니다. 이 때 유의할 점은 클래스 생성자 자체의 구현이나 밖에서 구현한 함수를 원형을 통해 명시하는 경우 클래스 선언의 public 영역 내에 포함되어야만 합니다.

 

그리고 3번째 SimpleClass 생성자를 일부러 함수 밖에 선언했는데, 이런 경우 클래스의 이름을 범위 지정 연산자를 통해 접근할 수 있도록 사용해야 합니다.

#include <iostream>
using namespace std;

class SimpleClass
{
private:
    int num1;
    int num2;
public:
    SimpleClass()       // 생성자 1
    {
        num1 = 0;
        num2 = 0;
    }
    SimpleClass(int n)  // 생성자 2
    {
        num1 = n;
        num2 = 0;
    }
    SimpleClass(int n1, int n2); // 생성자 3

    void Show() const { cout << num1 << ' ' << num2 << endl;  }
};

SimpleClass::SimpleClass(int n1, int n2)
{
    num1 = n1;
    num2 = n2;
}


int main(void)
{
    SimpleClass sc1;       // 생성자 1 호출. SimpleClass sc1()은 에러 발생하므로 주의
    sc1.Show();

    SimpleClass sc2(100);  // 생성자 2 호출
    sc2.Show();

    SimpleClass sc3(100, 200);  // 생성자 3 호출
    sc3.Show();

    return 0;
}
// 두 개의 생성자를 가진 Circle 클래스
#include <iostream>
using namespace std;

class Circle {
public:
	int radius;
	Circle();
	Circle(int r);
	double getArea();
};

Circle::Circle() {
	radius = 1;
	cout << "반지름 " << radius << " 원 생성" << endl;
}
Circle::Circle(int r) {
	radius = r;
	cout << "반지름 " << radius << " 원 생성" << endl;
}
double Circle::getArea() {
	return 3.14 * radius * radius;
}

int main() {
	Circle donut;
	// Circle donut(); -> 컴파일 오류
	double area = donut.getArea();
	cout << "donut 면적은 " << area << endl;

	Circle pizza(30);
	area = pizza.getArea();
	cout << "pizza 면적은 " << area << endl;
}

생성자를 호출한다고 donut() 을 사용하면 컴파일 오류가 나게 됩니다. 기본 생성자에 대해서는 클래스 이름, 즉 생성자 이름만 써주면 자동으로 매개변수가 없는 생성자가 호출되어 실행됩니다.

 

// Rectangle 클래스 만들기
#include <iostream>
using namespace std;

class Rectangle {
private:
	int width;
	int height;
public:
	Rectangle();
	Rectangle(int a);
	Rectangle(int a, int b);
	bool isSquare();
};

Rectangle::Rectangle() {
	width = 1;
	height = 1;
}

Rectangle::Rectangle(int a) {
	width = a;
	height = a;
}

Rectangle::Rectangle(int a, int b) {
	width = a;
	height = b;
}

bool Rectangle::isSquare() {
	if (width == height) return true;
	else return false;
}

int main() {
	Rectangle rect1;
	Rectangle rect2(3, 5);
	Rectangle rect3(3);

	if (rect1.isSquare()) cout << "rect1 은 정사각형이다." << endl;
	if (rect2.isSquare()) cout << "rect2 은 정사각형이다." << endl;
	if (rect3.isSquare()) cout << "rect3 은 정사각형이다." << endl;
}

3가지 형태의 생성자를 가지는 Rectangle class 입니다. 매개변수가 없는 경우, 매개변수가 2개인 경우, 매개변수가 1개인 경우로 나뉘어져 있습니다. 전반적인 선언과 구현 방법은 모두 동일합니다. 

 

 

 

디폴트 생성자(default constructor)

 

디폴트 생성자는 이름으로부터 유츄할 수 있듯 객체가 생성될 때 사용자가 초깃값을 명시하지 않으면, 컴파일러가 자동으로 제공하는 생성자 입니다. 디폴트 생성자는 사용자로부터 인수를 전달받지 않으므로, 매개변수를 가지지 않습니다. 매개변수를 가지지 않으므로 대부분의 디폴트 생성자가 0이나 NULL, 빈 문자열 등으로 초기화를 진행합니다.

 

컴파일러가 제공하는 디폴트 생성자의 원형은 아래와 같습니다.

SimpleClass::SimpleClass() { }

디폴트 생성자는 클래스에 생성자가 단 하나도 정의되지 않았을 때만, 컴파일러에 의해 자동으로 제공됩니다. 만약 사용자가 하나라도 생성자를 정의했다면 위 같은 객체의 선언은 오류를 발생시킵니다. 따라서 위처럼 초깃값을 명시하지 않고 객체를 생성하고 싶다면, 사용자가 직접 디폴트 생성자를 정의해야 합니다.

 

사용자가 직접 디폴트 생성자를 정의하는 방법은 2가지가 있습니다.

 

1. 디폴트 인수를 이용한 방법

2. 함수 오버로딩을 이용한 방법

 

먼저 디폴트 인수를 이용해 디폴트 생성자를 정의해봅시다. 기존 생성자의 모든 인수에 디폴트 인수를 명시함으로써 디폴트 생성자를 정의할 수 있습니다.

SimpleClass::SimpleClass(int n1 = 5, int n2 = 10);

위의 예제처럼 모든 인수에 디폴트 값을 명시하면, 인수를 전달하지 않고도 객체를 생성할 수 있는 디폴트 생성자가 됩니다.

 

함수 오버로딩을 이용한 디폴트 생성자는 아래 처럼 생성할 수 있습니다.

SimpleClass();

클래스는 단 하나의 디폴트 생성자만을 가질 수 있으므로, 위 방법 2개 중 한 가지 방법만으로 디폴트 생성자를 정의해야 합니다. 위 같은 방법으로 생성한 디폴트 생성자를 가지는 객체들은 여러 가지 방법으로 선언할 수 있습니다.

SimpleClass sc;	// 디폴트 생성자의 암시적 호출, sc() 라고 하면 오류가 발생한다.
SimpleClass sc = SimpleClass();	// 디폴트 생성자의 명시적 호출
SimpleClass *ptr_sc = new SimpleClass;	// 디폴트 생성자의 암시적 호출

 

 

복사 생성자(copy constructor)

 

새롭게 생성하는 변수에 다른 변수의 값을 대입하고 싶을 때 우리는 대입 연산자(=) 를 사용합니다. 마찬가지로 새롭게 생성하는 객체에 또 다른 객체의 값을 대입하기 위해서도 대입 연산자(=) 를 사용합니다.

 

그러나 대입 연산자를 이용한 객체의 대입은 얕은 복사(shallow copy) 로 수행됩니다. 얕은 복사란 값을 복사하는 것이 아닌 값을 가리키는 포인터를 복사 하는 것 입니다. 따라서 변수의 생성에서 대입 연산자를 이용한 값의 복사는 문제가 되지 않지만, 객체의 경우는 문제가 발생할 수도 있습니다. 특히 객체의 멤버가 메모리 공간의 힙 영역을 참조할 경우 문제가 발생합니다.

 

이러한 문제를 해결하기 위해 복사 생성자를 쓰는데, 복사 생성자란 자신과 같은 클래스 타입의 다른 객체에 대한 참조(reference) 를 인수로 전달받아 그 참조를 가지고 자신을 초기화 하는 방법 입니다.

 

복사 생성자는 새롭게 생성되는 객체가 원본 객체와 같으면서도, 완전한 독립성을 가지게 해줍니다. 복사 생성자를 통핸 대입은 깊은 복사(deep copy), 포인터가 아닌 값의 복사이기 때문입니다.

 

SimpleClass(const SimpleClass&);

 

복사 생성자는 객체가 함수에 인수로 전달될 때, 함수가 객체를 값으로 반환할 때, 새로운 객체를 같은 클래스 타입의 기존 객체와 똑같이 초기화 할 때 사용합니다.

 

 

소멸자(destructor)

 

C++ 에서 생성자는 객체 멤버의 초기화뿐 아니라 객체를 사용하기 위한 외부 환경까지도 초기화하는 역할을 합니다. 따라서 객체의 수명이 끝나면 생성자의 반대 역할을 수행할 멤버 함수가 필요한데 그 멤버 함수를 소멸자(destructor) 라고 합니다. 소멸자는 객체의 수명이 끝나면 컴파일러에 의해 자동으로 호출되며 사용이 끝난 객체를 정리합니다.

 

생성자와 마찬가지로 소멸자의 이름은 해당 클래스의 이름과 같으며, 이름 앞에 물결 표시(~) 를 붙여 생성자와 구분합니다. 즉 SimpleClass 클래스의 소멸자는 ~SimpleClass() 라는 이름을 가지게 됩니다.

 

소멸자는 아래와 같은 특징을 가집니다.

 

1. 인수를 가지지 않는다.

2. 반환값이 없지만 void 형 선언 X

3. 객체는 여러 개의 생성자를 가질 수 있지만, 소멸자는 단 하나만 가질 수 있다.

3. 소멸자는 const, volatile 또는 static으로 선언될 수 없지만, const, volatile 또는 static으로 선언된 객체의 호출을 위해서 호출될 수는 있다.

 


  • 소멸자
    • 객체가 소멸되는 시점에서 자동으로 호출되는 함수 → 객체를 소멸시키는 것이 목적
    • 안 써도 C++가 자동으로 객체를 소멸시키나 필요한 경우가 있다. 파일을 닫거나, 동적 메모리를 반환하거나, 네트워크를 닫는 경우 소멸자가 필요하다.
      • 오직 한번만 자동 호출, 임의로 호출할 수 없음
      • 객체 메모리 소멸 직전 호출
      • 리턴 타입도 없고 매개변수도 없다.
      • 클래스의 이름 앞에 ~ 를 붙여서 구현한다.
      • 생성자와 다르게 한 클래스 내에 오직 한개만 작성 가능 (중복 불가능)

소멸자는 객체를 소멸하는 것 이외의 수행할 작업은 없습니다. 보통 그래서 다음과 같이 선언합니다.

SimpleClass::~SimpleClass() {}

생성자와 마찬가지로 클래스 소멸자의 원형 혹은 선언은 클래스 선언의 public 영역에 포함되어야만 합니다.

 

소멸자의 호출 시기는 컴파일러가 알아서 처리합니다. C++ 에서 객체가 선언된 메모리 영역별로 소멸자가 호출되는 시기는 다음과 같습니다.

메모리 영역 소멸자 호출 시기
데이터 영역 해당 프로그램이 종료될 때
스택 영역 해당 객체가 정의된 블록을 벗어날 때
힙 영역 delete를 사용하여 해당 객체의 메모리를 반환할 때
임시 객체 임시 객체의 사용을 마쳤을 때

 

 

 

 생성자/소멸자 실행 순서

소멸자는 스택과 마찬가지로 생성된 것부터 객체를 소멸시키지 않고, 마지막에 선언한 객체를 우선적으로 소멸시킵니다. 이는 객체의 선언 및 동작이 스택 프레임과 연관되어 있기 때문입니다.

 

https://blog.naver.com/songsite123/223038823408

 

스택 프레임(Stack Frame)

지난 포스팅에서 다룬 스택에 대한 내용을 읽고 오시는 것을 추천드립니다. 이어지는 포스팅입니다. 스택 ...

blog.naver.com

 

위 포스팅을 읽었다는 가정 하에 예제 코드에서 어떤 순서로 생성자와 소멸자들이 실행되는지 보고 마치도록 하겠습니다.

MyClass a, b;
void f() {
	MyClass c;
}
int main() {
	f();
    MyClass d;
}

MyClass 클래스가 있다고 가정하면, 위 코드에 의해 a,b,c,d 객체의 생성자와 소멸자는 어떤 순서대로 실행될까요?

 

가장 먼저 global 객체로 선언된 a 와 b의 생성자가 실행될 것입니다. 그 이후 main 함수가 실행되며 f()가 실행되겠죠. f()가 실행되며 c의 생성자가 실행됩니다. 그 이후 d로 넘어가기 이전에 f의 함수가 끝나게 되면서 c의 소멸자가 바로 실행됩니다. 그 다음에 d의 생성자가 호출되고, main 함수가 끝나면서 d 소멸, b 소멸, a 소멸 의 순으로 소멸자가 실행됩니다.

 

즉 a 생성자 -> b 생성자 -> c 생성자 -> c 소멸자 -> d 생성자 -> d 소멸자 -> b 소멸자 -> a 소멸자 순으로 실행됩니다.

 


 

클래스를 사용하기 위해서는 생성자와 소멸자가 반드시 필요합니다. 따라서 중요한 개념이지만 포스팅이 너무 길어질 것 같아 예제 코드를 많이 추가하지는 않았습니다. 생성자와 소멸자만 확인하기보단 앞으로 클래스를 사용하다보면 자연스럽게 만나게 되는게 생성자와 소멸자이기 때문에 추후 포스팅에서 다른 개념의 예제에서 클래스를 사용해보며 생성자와 소멸자에 대해 더 익숙해져보도록 하겠습니다. 감사합니다.