본문 바로가기
공부

C++ 생성자 & 소멸자

by MY블로그 2022. 11. 23.

지난번 과제로 static 에 관하여 알아보고 공부하였다.

이번에는 C++의 생성자와 소멸자에 관하여 조사 하게 되었다.

아직 모르는 부분이기때문에 두가지의 분류를 나누어 조사 해보도록 한다.


● 생성자 ( Constructor :: 영문 사전 의미 - 제작자 )

생성자는 해당클래스의 객체가 인스턴스화될 때 자동으로 호출되는 특수한 종류의 멤버 함수다.

 

일반적으로 클래스의 멤버 변수를 적절한 기본값 또는 사용자 제공 값으로 초기화 하거나 클래스를 사용하는 데 필요한

설명 (ex. 파일 열기.etc)이 필요한 경우 사용된다.

 

일반적인 멤버 함수와 다르게 생성자 정의 방법에는 특정 규칙이 있다.

  1. 생성자 이름은 클래스와 이름이 같아야 한다.
  2. 생성자는 리턴 타입이 없다. (리턴이 없다고하여 void와 같다는 의미는 아니다! void와 다르다.)

클래스의 모든 멤버 변수가 모두 public 인 경우 초기화 목록(initialization list) 또는 유니폼 초기화(uniform initialization)를 사용해서 초기화를 직접 진행할 수 있다. (아래 코드 참조)

(주의. 멤버 변수가 class의 private 인 경우는 변수에 직접 접근x 이므로 아래처럼 초기화 불가능!)

// 코드 예시
#include <iostream>
using namespace std;

class Foo
{
public:
	int m_x;
	int m_y;
};

int main()
{
	Foo foo1 = { 4, 5 }; // 초기화 목록 (initialization list)
	Foo foo2{ 6, 7 }; // 유니폼 초기화 (uniform initialization)

	return 0;
}

 


 

1. 기본 생성자 ( Default constructor )

매개 변수를 갖지 않거나 모두 기본값이 설정된 매개 변수를 가지고 있는 생성자를 기본 생성자 라고 한다.

클래스를 인스턴트화할 때에 사용자가 초기값을 제공하지않으면 기본 생성자가 호출 된다.

// 코드 예시
#include <iostream>
using namespace std;

class Fraction
{
private:
	int m_numerator; // 분자
	int m_denominator; // 분모

public:
	Fraction() // 기본 생성자
	{
		m_numerator = 0; // 분자 정의
		m_denominator = 1; // 분모 정의
	}

	int getNumerator() { return m_numerator; }
	int getDenominator() { return m_denominator; }
	double getValue() { return static_cast<double>(m_numerator) / m_denominator; }
};

int main()
{
	Fraction frac; // 인수가 없으므로 Fraction() 기본 생성자를 호출
	cout << frac.getNumerator() << "/" << frac.getDenominator() << endl;

	return 0;
}

<출력 결과는 아래쪽 참조>

위 코드 예시는 분자와 분모값을 가진 Fraction 클래스가 있고, 클래스 이름과 같은 Fraction() 기본 생성자가 정의 되었다.

인수 없이 Fraction 타입의 객체를 *인스턴스화 했으므로 객체에 메모리가 할당된 직후 기본 생성자가 호출되고 객체가 초기화 된다.

분자와 분모변수는 기본 생성자에서 지정된 값으로 초기화되었다.

이처럼 기본 생성자는 대부분 클래스에서 매우 유용한 기능이다.

만약 기본 생성자가 없었다면 위 변수들은 값을 명시적으로 할당할 때까지 쓰레기값을 가지고 있었을 것이다.

(기본 자료형인 멤버 변수는 자동으로 초기화 되지 않는다!)

 

잠깐 궁금한점.

frac.getValue() 를 출력하면 어떻게 나올까??  결과는 0 이 나왔다. 궁금해서 그냥 해봤다!

 

↓↓↓접은글 참조  *인스턴스 ?

더보기

인스턴스(instance)는 같은 클래스에 속하는 개개의 객체로, 하나의 클래스에서 생성된 객체를 말한다.즉 클래스가 구체화되어, 클래스에서 정의된 속성과 성질을 가진 실제적인 객체로 표현된 것을 의미한다.이때 추상적인 개념인 클래스에서 실제 객체를 생성하는 것을 인스턴스화(instantiation)라고 한다.
[네이버 지식백과] 인스턴스(쉽게 배우는 소프트웨어 공학, 2015. 11. 30., 김치수)

 


 

2. 매개 변수가 있는 생성자를 사용한 초기화

기본 생성자는 클래스 멤버 변수의 기본값을 설정하고자 할때에 유용하지만,

클래스 인스턴스 별 멤버 변수의 값을 특정한 값으로 초기화하고 싶은 경우가 있다.

이때 생성자에 매개 변수를 선언 할 수 있다.

// 코드 예시
#include <iostream>
#include <cassert> // assert 사용을 위한 STL 추가
using namespace std;

class Fraction
{
private:
	int m_numerator; // 분자
	int m_denominator; // 분모

public:
	Fraction() // 기본 생성자
	{
		m_numerator = 0; // 분자
		m_denominator = 1; // 분모
	}

	// 두개의 매개변수가 있는 생성자, 하나의 매개변수는 기본값을 가진다.
	Fraction(int numerator, int denominator=1)
	{
		assert(denominator !=0);
		m_numerator = numerator;
		m_denominator = denominator;
	}

	int getNumerator() { return m_numerator; }
	int getDenominator() { return m_denominator; }
	double getValue() { return static_cast<double>(m_numerator) / m_denominator; }
};

↓↓↓접은글 참조   *assert 란?

위 예제에는 한 클래스 안에 두개의 생성자가 있다.

  • 첫번째로 기본 경우에 호출될 기본 생성자 
  • 두번째로 두개의 매개 변수를 사용하는 생성자

위의 두 생성자는 함수 오버로드로 인해 같은 클래스 안에서 공존할 수 있다.

실제 각각 고유한 서명(매개 변수 개수 및 타입)으로 원하는 수 만큼 생성자를 정의할 수 있다.

 

이 생성자를 매개 변수와 함께 사용하는 방법은 간단히 직접 초기화 형식을 사용하면 가능하다.

int x(5); // 정수를 직접 초기화 한다.
Fraction fiveThirds(5, 3); // Fraction을 직접 초기화하고 Fraction(int,int) 생성자 호출

// C++ 11에서는 유니폼 초기화 방식을 선호하기도 한다.
int x{ 5 }; // 정수의 균일한 초기화.
Fraction fiveThirds{ 5,3 }; // Fraction의 균일한 초기화 Fraction(int,int) 생성자 호출

// 위 예제의 매개 변수가 있는 생성자에서 두 번째 매개 변수는
// 기본값이 지정되어 있으므로 다음과 같은 방식도 유효
Fraction six(6); // Fraction(int,int) 생성자 호출, 두번째 매개 변수는 기본값 사용

 


 

3. 클래스와 대입 연산자 ( = ) 를 이용한 복사 초기화

기본 자료형인 변수와 마찬가지로 대입 연산자 ( = ) 를 이용해서 클래스를 초기화 할 수 있다.

int x = 6; // 복사 초기화 정수

Fraction six = Fraction(6); // 복사는 Fraction을 초기화하고 Fraction(6,1)을 호출

Fraction seven = 7; // 복사는 Fraction을 초기화하고
// 컴파일러는 7을 Fraction(7,1) 생성자를 호출하는 Fraction으로 변환하는 방법을 찾으려 한다

위처럼 초기화 할수 있으나 이런 방식의 초기화는 효율이 떨어지므로 사용하지 않는것이 좋다.

직접 초기화, 유니폼 초기화, 복사 초기화 모두 기본 자료형과는 같은 방식으로 작동하지만

복사 초기화는 클래스와는 다르게 동작 한다.

 


 

4. 생성자 줄이기

위쪽의 예제에 있는 두 생성자를 아래와 같이 하나로 단순화 시킬 수 있다.

// 코드 예시
#include <iostream>
#include <cassert>
using namespace std;

class Fraction
{
private:
	int m_numerator; // 분자
	int m_denominator; // 분모

public:
	// 기본 생성자
	Fraction(int numerator=0, int denominator=1) // numerator에 0대입
	{
		assert(denominator !=0);
		m_numerator = numerator;
		m_denominator = denominator;
	}

	int getNumerator() { return m_numerator; }
	int getDenominator() { return m_denominator; }
	double getValue() { return static_cast<double>(m_numerator) / m_denominator; }
};

이 생성자는 여전히 기본 생성자이지만 이제는 하나 또는 두개의 사용자제공 값을 허용할 수 있는 방식으로 정의 되었다.

Fraction zero; // Fraction(0,1)을 호출한다
Fraction six(6); // Fraction(6,1)을 호출한다
Fraction fiveThirds(5, 3); // Fraction(5,3)을 호출한다

 

5. 암시적으로 생성되는 기본 생성자

↓↓↓접은글 참조  /  암시적(implicit) 이란?

더보기

*암시적 의 정의보다 암시적 선언에대한 설명이 구체적이어 참조함
암시적 선언 이란,
명시적 선언이 없을 때 미리 약정된 규정대로 선언되는 것. 예를 들어, 포트란에서는 명시적 선언 없이도 변수들을 사용할 수 있는데 첫자가 I, J, K, L, M, N으로 시작하는 변수명은 정수형 변수로, 그렇지 않으면 실수형 변수로 암시적으로 선언된다. 이와 같이 암시적 선언을 허용하는 프로그램 언어는 프로그램을 작성하기는 쉽지만 오류를 범하기 쉽다. [네이버 지식백과] 암시적 선언 [implicit declaration, 暗示的宣言] (IT용어사전, 한국정보통신기술협회)

 

클래스에 다른 생성자가 없으면 C++ 컴파일러는 자동으로 기본 생성자를 생성한다.

이를 암시적 생성자라고 한다. 아래의 예제를 참고하자.

class Data
{
private:
	int m_year = 1900;
	int m_month = 1;
	int m_day = 1;
};

위 코드의 클래스에는 생성자가 없다. 그러므로 컴파일러는 아래와 같게 동작하는 생성자를 생성한다.

class Data
{
private:
	int m_year = 1900;
	int m_month = 1;
	int m_day = 1;

public:
	Data() // 암시적으로 생성된 생성자
	{

	}
};

암시적으로 생성되는 기본 생성자를 사용하면 매개 변수 없이 Data 객체를 만들 수 있으나 멤버를 초기화하지는 않는다.

(모든 멤버 변수가 기본 자료형이며 생성 시 초기화하지 않으므로)

일반적으로 클래스의 객체를 만드는 법을 명시적으로 나타내기 위해 항상 하나 이상의 생성자를 정의 하는것이 좋다.

(가독성을 위해서라도)

 


 

6. 클래스를 포함하는 클래스

클래스는 다른 클래스를 멤버 변수로 포함할 수 있다.

기본적으로 외부 클래스가 생성될 때 멤버 변수는 기본 생성자가 호출 되는데,

이것은 생성자의 본문이 실행되기 전에 발생한다.

// 코드 예시
#include <iostream>
#include <cassert>
using namespace std;


class A
{
public:
	A() { cout << "A\n"; }
};

class B
{
private:
	A m_a; // 클래스 B 는 클래스 A 를 멤버 변수로 포함한다.

public:
	B() { cout << "B\n"; }
};


int main()
{
	B b;
	return 0;
}

<위 코드의 출력 결과>

변수 b가 생성되면 B() 생성자가 호출된다.

생성자의 본문이 실행되기 전에 m_a 가 초기화되어 클래스 A의 기본 생성자 A()가 호출된다.

그리하여 "A"가 출력된 다음으로 제어가 B 생성자로 돌아가서 본문이 실행된다.

B() 생성자가 변수 m_a 를 사용하고 싶을 수도 있으므로 m_a 를 먼저 초기화 하는 것이 좋다.

 

7. 요약
  • 컴파일러는 생성자 호출 전에 객체에 대한 메모리를 할당하지 않는다.
  • 생성자는 두가지 목적으로 사용 된다.
  1. 누가 객체를 만들 수 있는지 결정한다.
  2. 객체를 초기화 할 수있다.


추가적으로 참조하자! 생성자 멤버 초기화 리스트 (Constructor member initializer list)

아직 확실히 생성자에 대하여 파악하지 못하였기에 참조시켜본다. 하나씩 파악한후 참조하자!

https://boycoding.tistory.com/246

 

C++ 09.06 - 생성자 멤버 초기화 리스트 (Constructor member initializer list)

생성자 멤버 초기화 리스트 (Constructor member initializer list) 이전 포스트 생성자에서 대입 연산자(=)를 사용하여 클래스 멤버 변수를 초기화했다: class Something { private: int m_value1; double m_value2; char m_val

boycoding.tistory.com



● 소멸자 ( Destructor :: 영문 사전 의미 - 소멸, 소각, 파괴 )

소멸자는 객체가 소멸될 때 자동으로 실행되는 클래스 멤버 함수이다.

 

생성자는 클래스의 초기화를 돕도록 설계되어있지만 소멸자는 청소를 돕도록 설계되어 있다.

(ex. 예전 가비지컬렉터에 대하여 알아본바 있다. C++에는없고 JAVA에 있는기능이었다. C++에서 가비지를 관리하는것이 소멸자 이다!)

 

지역에서 생성된 객체가 지역 범위를 벗어나거나 동적으로 할당된 객체가 삭제 키워드를 사용해 명시적으로 삭제되면,

객체가 메모리에서 제거되기 전에 필요한 정리를 수행하기 위해 클래스는 소멸자가 있는 경우 소멸자를 자동호출한다.

 

클래스의 멤버변수들이 단순하게 기본 자료형이 값 형식이라면 크게 필요없으나

다른 리소스(ex. 동적 메모리, 파일 또는 데이터베이스 핸들러)라면 객체가 소멸되기전 어떤 종류의 유지보수를 해야 한다.

이때 소멸자는 객체가 소멸되기 전에 마지막으로 호출되는 특별한 함수이므로 완벽한 장소가 된다.


1. 소멸자 규칙

생성자처럼 소멸자도 특별한 규칙이 있다.

  1. 소멸자 이름은 클래스 이름과 같아야 하며 앞쪽에 ~ 가 붙는다.
  2. 소멸자는 인수가 없다.
  3. 소멸자는 반환값이 없다.

이런 규칙 때문에 소멸자는 클래스당 하나밖에 존재할 수 없다.

또한, 소멸자를 명시적으로 호출하는 경우는 없다.

<사용 예제>

// 코드예시
#include <iostream>
#include <cstddef>

using namespace std;

class IntArray
{
private:
    int* m_Array;
    int m_Length;

public:
    IntArray(int length) // 생성자
    {
        m_Array = new int[static_cast<size_t>(length)]{};
        m_Length = length;
    }

    ~IntArray() // 소멸자
    {
        // 동적으로 할당한 배열을 삭제한다.
        delete[] m_Array;
    }

    void SetValue(int index, int value)
    {
        m_Array[index] = value;
    }

    int GetValue(int index)
    {
        return m_Array[index];
    }

    int GetLength()
    {
        return m_Length;
    }
};

int main()
{
    IntArray ar(10);

    for (int count = 0; count < ar.GetLength(); ++count)
    {
        ar.SetValue(count, count + 1);
    }

    cout << "The value of element 5 is: " << ar.GetValue(5) << '\n';
    // 출력 -> The value of element 5 is: 6

    return 0;
} // ar 객체는 여기서 삭제되므로 ~IntArray() 소멸자 함수는 여기서 호출된다.
  • IntArray ar(10); 에서 ar 이라는 새로운 IntArray 클래스의 객체를 인스턴스화 하고 10의 길이를 가진 배열 동적할당.
  • main() 함수의 끝에서 ar 객체는 스코프 범위를 벗어난다.
  • 이로 인해 ~IntArray() 소멸자가 호출되어 생성자에서 할당한 배열이 삭제 된다.

 


 

2. 생성자 및 소멸자의 생명주기

지금까지 설명한 것처럼 객체가 생성되는 시점에는 생성자를,

객체가 소멸되는 시점에는 소멸자를 호출한다.

 

다음의 코드 예제 에서는 생성자와 소멸자 내부에서 cout 을 이용하여 이를 더 자세하게 알아볼수 있다.

// 코드예시
#include <iostream>
#include <cstddef>

using namespace std;

class Simple
{
private:
    int m_ID;

public:
    Simple(int id) :m_ID(id)
    {
        cout << "생성 Simple" << m_ID << endl;
    }

    ~Simple() // 소멸자
    {
        cout << "소멸 Simple" << m_ID << endl;
    }

    int GetID()
    {
        return m_ID;
    }
};

int main()
{
    // 스택에 단순 할당
    Simple simple(1);
    cout << simple.GetID() << endl;

    // 동적 할당
    Simple* pSimple = new Simple(2);
    cout << pSimple->GetID() << endl;

    // pSimple을 동적할당 하였기때문에 사용후 삭제
    delete pSimple;

    return 0;
}

< 코드 출력 결과 >

생성 Simple1 // 스택 영역에 단순 할당된 simple 객체(인스턴스)의 생성자
1
생성 Simple2 // 힙 영역에 동적 할당된 pSimple 객체(인스턴스)의 생성자
2
소멸 Simple2 // 명시적으로 pSimple 객체 삭제 -> pSimple 객체(인스턴스)의 소멸자
소멸 Simple1 // main 함수의 스택을 벗어 났으므로 simple 객체 삭제 -> simple 객체의(인스턴스)의 소멸자

main() 함수가 끝나기 전에 pSimple 을 명시적으로 삭제했으므로 '소멸 Simple2' 의 출력문을 먼저 볼 수 있다.

 


 

3. RAII ( Resource Acquisition Is Initialization -> 직역 : 리소스 획득은 초기화 입니다. )

RAII 는 객체의 수명과 리소스 관리와 연관있는 프로그래밍 디자인 기법이다.

C++ 에서 RAII 는 클래스의 생성자와 소멸자로 구현되어있다.

  • 리소스(메모리,파일 또는 데이터베이스 핸들 등)는 일반적으로 객체의 생성자에서 획득된다.
  • 그 리소스는 객체가 살아있는 동안 사용될 수 있다.
  • 객체가 소멸하면 자원이 소멸된다.

RAII 의 장점은 리소스를 보유한 객체가 자동으로 정리됨에 따라 리소스 누수 ( ex : Memory Leak / 메모리 누수 ) 를

방지하는 것에 도움이 된다.

 


 

4. 요약

생성자와 소멸자를 함께 사용하면 프로그래머가 특별한 작업을 하지 않아도 클래스는 스스로 초기화하고 정리할 수 있다.

이렇게 한다면 오류가 발생할 확률을 보다 낮출 수 있으며 클래스를 더 쉽게 사용할 수 있다.

댓글