본문 바로가기
Object Oriented Programming(C++)/열혈 C++ 프로그래밍

C++ | 04-3 생성자와 소멸자(Constructor and Destructor)

by continue96 2021. 8. 10.

1. 생성자

1.1 생성자의 선언과 정의

 새로운 객체를 생성하고 멤버 변수를 private으로 선언하면 이 멤버 변수를 초기하는 멤버 함수를 부득이하게 정의해야 했다. 그러나 멤버 변수를 초기화하기 위해 매번 멤버 함수를 정의하는 것은 여간 번거로운 일이 아니다. 다행히 C++는 특별한 멤버 함수, 생성자(Constructor)를 제공한다. 생성자는 객체의 생성과 동시에 멤버 변수를 초기화해주는 멤버 함수로 객체를 생성할 때 딱 한 번만 호출된다. 생성자를 정의하려면 다음과 같은 형식을 따르면 된다.

 

① 클래스의 이름과 같다.

 생성자는 클래스의 이름과 완전히 같고, 보통 public으로 선언한다.

class Overwatch {
private:
	string name;
	int age;
    
public:
	/* 생성자는 클래스의 이름과 같습니다. */
	Overwatch() {
		name = "Unknown";
		age = -1;
	}
};

 

② 반환형이 선언되어 있지 않다.

 생성자는 멤버 변수를 초기화하는 함수이므로 아무것도 반환하지 않는다. 따라서 반환형이 선언되어 있지 않고 void로도 선언되어 있지 않다.

class Overwatch {
private:
	string map;
	int round;
    
public:
	/* 생성자는 반환형이 없습니다. */
	Overwatch() {
		map = "Unknown";
		round = -1;
	}
};

 

③ 함수 오버로딩을 할 수 있다.

 생성자는 멤버 함수의 일종이기 때문에 함수 오버로딩을 할 수 있다. 즉, 함수 오버로딩의 규칙에 따라 매개 변수의 자료형과 개수를 다르게 한다면 여러 생성자를 오버로딩하여 정의할 수 있다.

class Overwatch {
private:
	string map;
	int round;
    
public:
	Overwatch() {
		map = "Unknown";
		round = -1;
	}
    
	/* 오버로딩된 생성자 */
	Overwatch(string _map, int _round) {
		map = _map;
		round = _round;
	}
};

 

④ 매개 변수 디폴트 값을 설정할 수 있다.

 생성자는 멤버 함수의 일종이기 때문에 매개 변수에 디폴트 값을 설정할 수 있다. 규칙에 따라 오른쪽 매개 변수부터 디폴트 값을 채우면 매개 변수에 인자를 전달하지 않았을 때, 미리 설정한 디폴트 값이 대신 전달된다.

class Overwatch {
private:
	string map;
	int round;
    
public:
	/* 매개 변수에 디폴트 값이 설정된 생성자 */
	Overwatch(string _map = "Unknown", int _round = -1) {
		map = _map;
		round = _round;
	}
};

 

[한줄 요약] 생성자란?
생성자는 새로 생성된 객체를 초기화하기 위해 호출되는 특별한 멤버 함수이다.

 

1.2 생성자의 호출

 새로운 객체를 생성할 때 생성자를 호출하여 객체를 초기화할 수 있다는 것을 배웠다. 그렇다면 생성자를 어떻게 호출해야 새로운 객체가 생성될까? 우선 매개 변수가 있는 생성자의 경우, 매개 변수에 전달할 인자를 포함하여 다음 1, 2행과 같은 방법으로 생성자를 호출해야 한다.

ClassName ObjectName(argument, argument, ...); /* 객체 생성 */
ClassName* ObjectName = new ClassName(argument, argument, ...); /* 동적할당된 객체 생성 */

 

그러나 매개 변수가 없는 생성자의 경우, 다음 1행과 같은 방법으로 생성자를 호출하면 문제가 발생하므로 2, 3, 4행과 같은 방법으로 생성자를 호출해야 한다.

ClassName ObjectName(); /* 객체를 반환하는 함수 선언 */
ClassName ObjectName; /* 객체 생성 */
ClassName* ObjectName = new ClassName(); /* 동적할당된 객체 생성 */
ClassName* ObjectName = new ClassName; /* (소괄호 생략) 동적할당된 객체 생성 */

 

 매개 변수가 선언되어 있지 않으니 4행의 소괄호의 생략은 충분히 이해할 수 있다. 하지만 소괄호를 생략하지 않은 1행은 왜 문제가 발생하는 것일까? 이것은 함수 선언과 관련되어 있다. 우리가 함수를 선언할 때, main 함수 밖에서 전역적으로 선언하는 것이 일반적이다. 하지만 main 함수 안에서 함수를 지역적으로 선언할 수도 있다. 1행을 살펴보면 매개 변수가 없고 객체를 반환하는 ObjectName의 함수를 지역적으로 선언하는 것으로 볼 수 있다. C++는 이렇게 생성자의 호출과 함수의 선언, 이 두 가지로 해석될 수 있는 문장은 생성자의 호출이 아닌 매개 변수가 없고 객체를 반환하는 함수의 선언으로 컴파일하도록 약속했다. 따라서 매개 변수가 선언되어 있지 않은 생성자를 호출하려면 2행과 같이 소괄호를 반드시 생략해야 한다.

 

Calculator.cpp
#include <iostream>
using namespace std;

class Calculator {
private:
	int A;
	int B;
    
public:
	/* 기본 생성자 */
	Calculator() {
		A = 0;
		B = 0;
		cout << "기본 생성자가 호출되었습니다." << endl;
	}

	/* 오버로딩된 생성자 */
	Calculator(int _A, int _B) {
		A = _A;
        B = _B;
		cout << "오버로딩된 생성자가 호출되었습니다." << endl;
	}
};

int main(void) {
	/* 기본 생성자로 객체를 생성합니다. */
	Calculator cal1(); /* cal1 함수를 선언합니다. */
	Calculator cal2;
   	Calculator* cal3 = new Calculator();
	Calculator* cal4 = new Calculator;
	return 0;
}

 /* cal1 함수를 정의합니다. */
Calculator cal1() {
	Calculator cal(10, 20);
	return cal;
}

 

Calculator.cpp 실행 결과
기본 생성자가 호출되었습니다.
기본 생성자가 호출되었습니다.
기본 생성자가 호출되었습니다.

 위의 예제에서 27행은 cal1 함수의 선언이므로 생성자가 호출되지 않는다. 따라서 27행을 제외한 28, 29, 30행에서 기본 생성자가 세 번 호출된다.

 

[한줄 요약] 생성자의 호출
생성자를 호출하여 객체를 선언하고 초기화할 때 소괄호 안에 매개 변수에 알맞은 인자를 전달해야 한다. 단, 매개 변수가 없는 생성자의 경우, 소괄호를 반드시 생략한다.

 

1.3 멤버 이니셜라이저

 멤버 이니셜라이저(Member Initializer)는 멤버 변수를 초기화하는 생성자에서 사용할 수 있는 문법으로, 생성자의 몸체에서 다소 불편하게 멤버 변수를 초기화하는 것보다 조금 더 쉽게 멤버 변수를 초기화할 수 있다. 이와 더불어, 멤버 이니셜라이저는 생성자의 몸체에서 초기화를하는 것보다 다음과 같은 더 많은 역할을 제공한다.

 

① 멤버 변수의 초기화

 멤버 이니셜라이저는 생성자의 몸체에서 멤버 변수를 초기화하는 것보다 훨씬 더 간단하게 멤버 변수를 초기화할 수 있다. 이때, 멤버 변수의 이름과 매개 변수의 이름이 같더라도 자동으로 두 변수를 구분하기 때문에 두 변수의 이름이 같아도 된다. 또한, 멤버 이니셜라이저를 사용하면 생성자의 몸체가 텅 빌 수 있으나 이것은 전혀 문제가 되지 않는다.

class Rectangle {
private:
	int width;
	int height;

public:
	/* 멤버 이니셜라이저로 멤버 변수를 초기화합니다. */
	Rectangle(int _width, int _height) : width(_width), height(_height) { }
};

 

② const 멤버 변수의 초기화

 생성자의 몸체에서 대입 연산자를 통해 멤버 변수를 초기화하면 선언과 초기화가 각각 분리된 형태로 바이너리 코드가 생성된다. 하지만 멤버 이니셜라이저를 사용하면 선언과 초기화가 합쳐진 하나의 형태로 바이너리 코드가 생성된다. 따라서 선언과 동시에 초기화돼야 하는 const 변수를 멤버 변수로 선언할 때, 멤버 이니셜라이저를 사용하여 const 변수를 초기화할 수 있다.

class Circle {
private:
	const double pi; /* const 멤버 변수 pi */
	int radius;

public:
	/* 멤버 이니셜라이저로 const 멤버 변수 pi를 초기화합니다. */
	Circle(int _radius) : pi(3.14), radius(_radius) { }
};

 

참조자 멤버 변수의 초기화

 const 변수와 마찬가지로 선언과 동시에 초기화되어야 하는 참조자를 멤버 변수로 선언할 때, 멤버 이니셜라이저를 사용하여 참조자를 초기화할 수 있다.

#include <iostream>
using namespace std;

class Point {
private:
	int x, y;

public:
	/* 괄호 밖의 x는 멤버 변수로, 괄호 안의 x는 매개 변수로 자동으로 구분해줍니다. */
	Point(int x, int y) : x(x), y(y) { }
};

class Circle {
private:
	const double pi;
	const int& radius; /* 참조 멤버 변수 */
	Point& point; /* 객체 참조 멤버 변수 */

public:
	/* 멤버 이니셜라이저로 참조자 멤버 변수를 초기화합니다. */
	Circle(Point& p, const int& r) : pi(3.14), radius(r), point(p) { }
};

int main(void) {
	Point p(10, 20);
	Circle c(p, 10);
	return 0;
}

 

④ 객체 멤버 변수의 초기화

 객체 A의 멤버 변수로 또 다른 객체 B가 선언되었을 때, 객체 A를 생성하면 객체 B가 함께 생성된다. 멤버 이니셜라이저를 사용하면 객체 A를 생성하는 과정에서 객체 B의 생성자를 호출하여 객체 B의 멤버 변수를 함께 초기화할 수 있다.

class Point {
private:
	int x;
	int y;

public:
	/* 멤버 이니셜라이저로 멤버 변수를 초기화합니다. */
	Point(int x, int y) : x(x), y(y) { }
};

class Line {
private:
	Point point1; /* Point 객체 선언 */
	Point point2; /* Point 객체 선언 */

public:
	/* 멤버 이니셜라이저로 객체 멤버 변수를 초기화합니다. */
	Line(int x1, int y1, int x2, int y2): point1(x1, y1), point2(x2, y2) { }
};

 

⑤ 부모 클래스 멤버 변수의 초기화

 자식 클래스의 생성자는 자신의 멤버 변수와 더불어 부모 클래스의 멤버 변수를 함께 초기화해야 한다. 따라서 자식 클래스의 생성자는 멤버 이니셜라이저를 통해 부모 클래스의 생성자를 호출하여 멤버 변수를 초기화한다.

class Square {
private:
	int width; /* 정사각형의 가로 */
	int depth; /* 정사각형의 세로 */

public:
	/* 멤버 이니셜라이저로 멤버 변수를 초기화합니다. */
	Square(int width, int depth) : width(width), depth(depth) { }
};

class Cube : public Square {
private:
	int height; /* 정육각형의 높이 */

public:
	/* 멤버 이니셜라이저로 부모 클래스의 생성자를 호출합니다. */
	Cube(int width, int height, int depth) : Square(width, depth), height(height) { }
};

 

[한줄 요약] 멤버 이니셜라이저
1. 멤버 변수 초기화
2. const 멤버 변수 초기화
3. 참조자 멤버 변수 초기화
4. 객체 멤버 변수 초기화
5. 부모 클래스의 멤버 변수 초기화

 

1.4 디폴트 생성자

 C++는 객체를 생성할 때 메모리 공간을 할당하고 생성자를 호출하는 두 단계를 반드시 거친다. 때문에 사용자가 생성자를 따로 정의하지 않은 클래스는 C++ 컴파일러가 자동으로 디폴트 생성자(Default Constructor)를 삽입하여 객체를 생성한다. 이때 디폴트 생성자는 사용자로부터 인수를 전달받지 않아 매개 변수가 없으며 함수 몸체가 텅 비어 있는 구조를 가지고 있다.

className() { } /* 디폴트 생성자 */

 

DefaultConstructor.cpp
#include <iostream>
using namespace std;

class Fallout {
private:
	string director;
	int year;

public:
	// Fallout() : director("토도키 하와도"), year(2008) { }; /* 디폴트 생성자를 명시합니다. */
	Fallout(string director, int year) : director(director), year(year) { }
};

int main(void) {
	Fallout Fallout1; /* 컴파일 오류! Fallout 클래스의 디폴트 생성자가 없습니다. */
	Fallout fallout3("토도키 하와도", 2008);
	return 0;
}

 위의 예제에서 11행을 살펴보면 사용자가 생성자를 정의하였기 때문에 디폴트 생성자가 자동으로 삽입되지 않는다. 15행은 객체 생성 과정에서 디폴트 생성자를 호출하는데, 디폴트 생성자가 정의되어있지 않으므로 컴파일 오류를 발생시킨다. 따라서 15행처럼 객체를 생성하길 원한다면 10행의 주석을 해제해야 한다.


2. 소멸자

2.1 소멸자의 선언과 정의

 생성자가 객체의 멤버 변수를 초기화하는데 도움을 주는 함수였다면 소멸자(Destructor)는 생성한 객체를 삭제하는데 도움을 주는 특별한 함수이다. 소멸자는 객체를 소멸해주는 멤버 함수로 객체를 소멸할 때 딱 한 번만 호출된다. 소멸자를 정의하려면 다음과 같은 형식을 따르면 된다.

 

① 클래스의 이름 앞에 ~가 붙은 것과 같다.

 소멸자는 클래스와 이름이 같고 앞에 ~가 붙어있다.

 

② 매개 변수가 없고, 반환형이 선언되어 있지 않다.

 소멸자는 매개 변수가 없어 함수 오버로딩을 할 수 없고 디폴트 매개 변수를 설정할 수도 없다. 즉, 소멸자는 클래스에 딱 하나만 존재해야 한다.

class Overwatch {
private:
	string map;
	int round;
    
public:
	Overwatch() {
		map = "Unknown";
		round = -1;
	}
    
	/* 소멸자는 ~클래스 이름과 같습니다. */
	~Overwatch() { }
};

 

③ 소멸자는 생성자에서 할당한 리소스를 해제해야 한다.

 소멸자는 생성자에서 할당한 메모리, 파일, 그리고 데이터베이스 핸들 등 리소스를 해제해야 한다. 특히, new 연산자를 사용하여 메모리를 동적할당받은 멤버 변수는 소멸자에서 delete 연산자를 사용해 할당받은 공간을 소멸해야 한다.

class Overwatch {
private:
	string map;
	int* round;

public:
	Overwatch(string map, int _round) : map(map) {
		round = new int[_round]; /* 라운드 배열을 동적할당합니다. */
	}

	/* 소멸자는 ~클래스 이름과 같습니다. */
	~Overwatch() {
		delete[] round; /* 라운드 배열을 해제합니다. */
	}
};

 

[한줄 요약] 소멸자란?
생성된 객체를 삭제하기 위해 호출되는 특별한 멤버 함수이다.

 

2.2 디폴트 소멸자

 C++는 디폴트 생성자와 마찬가지로 객체를 소멸할 때 소멸자를 반드시 호출한다. 때문에 사용자가 소멸자를 따로 정의하지 않은 클래스는 C++ 컴파일러가 자동으로 디폴트 소멸자(Default Destructor)를 삽입하여 객체를 소멸한다. 디폴트 소멸자는 매개 변수가 없으며 함수 몸체가 텅 비어 있는 구조를 가지고 있다.

~className() { } /* 디폴트 소멸자 */

3. C++ 기반의 데이터 입출력 문제 풀이

 

열혈 C++ 프로그래밍 | 문제 04-3 | C++ 기반의 데이터 입출력

문제 1 앞서 제시한 문제 04-2를 해결하였는가? 당시만 해도 생성자를 설명하지 않은 상황이었기 때문에 별도의 초기화 함수를 정의 및 호출해서 Point, Circle, Ring 클래스의 객체를 초기화하였다.

continue96.tistory.com

댓글